Skip to content

Commit 570f565

Browse files
committed
refactor(components): split SettingsTab into per-section files
Extract Language Model, Context Window, Semantic Search, Knowledge Base, Advanced Search, Template Variables, and Jira sections into dedicated sibling files under sections/. The SettingsTab shell now orchestrates these section components along with the previously extracted ops and overview sections. Each section owns the props it needs; variable-form state lives inside VariablesSection, jira-form state lives inside JiraSection, and show-other-models toggle lives inside ModelSection.
1 parent 50031a6 commit 570f565

8 files changed

Lines changed: 1164 additions & 952 deletions

File tree

src/components/Settings/SettingsTab.tsx

Lines changed: 118 additions & 952 deletions
Large diffs are not rendered by default.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
interface AdvancedSearchSectionProps {
2+
vectorEnabled: boolean;
3+
onVectorToggle: () => void;
4+
}
5+
6+
export function AdvancedSearchSection({
7+
vectorEnabled,
8+
onVectorToggle,
9+
}: AdvancedSearchSectionProps) {
10+
return (
11+
<section className="settings-section">
12+
<h2>Advanced Search</h2>
13+
<p className="settings-description">
14+
Enable AI-powered semantic search for better knowledge base results.
15+
</p>
16+
<div className="vector-consent">
17+
<label className="toggle-label">
18+
<input
19+
type="checkbox"
20+
checked={vectorEnabled}
21+
onChange={onVectorToggle}
22+
/>
23+
<span className="toggle-text">Enable vector embeddings</span>
24+
</label>
25+
<p className="setting-note">
26+
Creates embeddings of your documents for semantic search. All
27+
processing happens locally on your machine.
28+
</p>
29+
</div>
30+
</section>
31+
);
32+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const CONTEXT_WINDOW_OPTIONS = [
2+
{ value: null, label: "Model Default" },
3+
{ value: 2048, label: "2K (2,048 tokens)" },
4+
{ value: 4096, label: "4K (4,096 tokens)" },
5+
{ value: 8192, label: "8K (8,192 tokens)" },
6+
{ value: 16384, label: "16K (16,384 tokens)" },
7+
{ value: 32768, label: "32K (32,768 tokens)" },
8+
];
9+
10+
interface ContextWindowSectionProps {
11+
loadedModel: string | null;
12+
contextWindowSize: number | null;
13+
onContextWindowChange: (value: string) => void;
14+
}
15+
16+
export function ContextWindowSection({
17+
loadedModel,
18+
contextWindowSize,
19+
onContextWindowChange,
20+
}: ContextWindowSectionProps) {
21+
return (
22+
<section className="settings-section">
23+
<h2>Context Window</h2>
24+
<p className="settings-description">
25+
Configure the maximum context length for LLM generation. Larger values
26+
allow more content but use more memory.
27+
</p>
28+
<div className="context-window-config">
29+
<select
30+
className="context-window-select"
31+
aria-label="Context window size"
32+
value={contextWindowSize ?? ""}
33+
onChange={(e) => onContextWindowChange(e.target.value)}
34+
disabled={!loadedModel}
35+
>
36+
{CONTEXT_WINDOW_OPTIONS.map((opt) => (
37+
<option key={opt.value ?? "default"} value={opt.value ?? ""}>
38+
{opt.label}
39+
</option>
40+
))}
41+
</select>
42+
{!loadedModel && (
43+
<p className="setting-note">
44+
Load a model to configure context window.
45+
</p>
46+
)}
47+
<p className="setting-note">
48+
Higher values require more RAM. The "Model Default" option uses the
49+
model's training context (capped at 8K).
50+
</p>
51+
</div>
52+
</section>
53+
);
54+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { useState } from "react";
2+
import { Button } from "../../shared/Button";
3+
4+
interface JiraSectionProps {
5+
jiraConfigured: boolean;
6+
jiraConfig: { base_url?: string; email?: string } | null;
7+
jiraLoading: boolean;
8+
onJiraConnect: (
9+
baseUrl: string,
10+
email: string,
11+
apiToken: string,
12+
) => Promise<void>;
13+
onJiraDisconnect: () => Promise<void>;
14+
}
15+
16+
export function JiraSection({
17+
jiraConfigured,
18+
jiraConfig,
19+
jiraLoading,
20+
onJiraConnect,
21+
onJiraDisconnect,
22+
}: JiraSectionProps) {
23+
const [jiraForm, setJiraForm] = useState({
24+
baseUrl: "",
25+
email: "",
26+
apiToken: "",
27+
});
28+
29+
async function handleSubmit(event: React.FormEvent) {
30+
event.preventDefault();
31+
await onJiraConnect(jiraForm.baseUrl, jiraForm.email, jiraForm.apiToken);
32+
setJiraForm({ baseUrl: "", email: "", apiToken: "" });
33+
}
34+
35+
return (
36+
<section className="settings-section">
37+
<h2>Jira Integration</h2>
38+
<p className="settings-description">
39+
Connect to Jira Cloud to import tickets directly into your drafts.
40+
</p>
41+
42+
{jiraConfigured ? (
43+
<div className="jira-connected">
44+
<div className="jira-status">
45+
<span className="status-icon">&#10003;</span>
46+
<span>Connected to {jiraConfig?.base_url || "Jira"}</span>
47+
</div>
48+
<p className="jira-email">Account: {jiraConfig?.email}</p>
49+
<Button
50+
variant="secondary"
51+
size="small"
52+
onClick={() => {
53+
void onJiraDisconnect();
54+
}}
55+
disabled={jiraLoading}
56+
>
57+
Disconnect
58+
</Button>
59+
</div>
60+
) : (
61+
<form className="jira-form" onSubmit={handleSubmit}>
62+
<div className="form-field">
63+
<label htmlFor="jira-url">Jira URL</label>
64+
<input
65+
id="jira-url"
66+
type="url"
67+
placeholder="https://your-company.atlassian.net"
68+
value={jiraForm.baseUrl}
69+
onChange={(e) =>
70+
setJiraForm((f) => ({ ...f, baseUrl: e.target.value }))
71+
}
72+
required
73+
/>
74+
</div>
75+
<div className="form-field">
76+
<label htmlFor="jira-email">Email</label>
77+
<input
78+
id="jira-email"
79+
type="email"
80+
placeholder="your.email@company.com"
81+
value={jiraForm.email}
82+
onChange={(e) =>
83+
setJiraForm((f) => ({ ...f, email: e.target.value }))
84+
}
85+
required
86+
/>
87+
</div>
88+
<div className="form-field">
89+
<label htmlFor="jira-token">API Token</label>
90+
<input
91+
id="jira-token"
92+
type="password"
93+
placeholder="Your Jira API token"
94+
value={jiraForm.apiToken}
95+
onChange={(e) =>
96+
setJiraForm((f) => ({ ...f, apiToken: e.target.value }))
97+
}
98+
required
99+
/>
100+
<p className="field-hint">
101+
Generate at{" "}
102+
<a
103+
href="https://id.atlassian.com/manage/api-tokens"
104+
target="_blank"
105+
rel="noopener noreferrer"
106+
>
107+
id.atlassian.com/manage/api-tokens
108+
</a>
109+
</p>
110+
</div>
111+
<Button
112+
type="submit"
113+
variant="primary"
114+
disabled={
115+
jiraLoading ||
116+
!jiraForm.baseUrl ||
117+
!jiraForm.email ||
118+
!jiraForm.apiToken
119+
}
120+
>
121+
{jiraLoading ? "Connecting..." : "Connect"}
122+
</Button>
123+
</form>
124+
)}
125+
</section>
126+
);
127+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Button } from "../../shared/Button";
2+
3+
interface KbSectionProps {
4+
kbFolder: string | null;
5+
indexStats: { total_chunks: number; total_files: number } | null;
6+
loading: string | null;
7+
onSelectKbFolder: () => void;
8+
onRebuildIndex: () => void;
9+
}
10+
11+
export function KbSection({
12+
kbFolder,
13+
indexStats,
14+
loading,
15+
onSelectKbFolder,
16+
onRebuildIndex,
17+
}: KbSectionProps) {
18+
return (
19+
<section className="settings-section">
20+
<h2>Knowledge Base</h2>
21+
<p className="settings-description">
22+
Configure the folder containing your knowledge base documents.
23+
</p>
24+
25+
<div className="kb-config">
26+
<div className="kb-folder-row">
27+
<div className="kb-folder-display">
28+
{kbFolder ? (
29+
<code>{kbFolder}</code>
30+
) : (
31+
<span className="kb-placeholder">No folder selected</span>
32+
)}
33+
</div>
34+
<Button variant="secondary" onClick={onSelectKbFolder}>
35+
{kbFolder ? "Change" : "Select Folder"}
36+
</Button>
37+
</div>
38+
39+
{kbFolder && (
40+
<div className="kb-stats">
41+
<div className="stat-item">
42+
<span className="stat-label">Files indexed</span>
43+
<span className="stat-value">
44+
{indexStats?.total_files ?? "—"}
45+
</span>
46+
</div>
47+
<div className="stat-item">
48+
<span className="stat-label">Total chunks</span>
49+
<span className="stat-value">
50+
{indexStats?.total_chunks ?? "—"}
51+
</span>
52+
</div>
53+
<Button
54+
variant="ghost"
55+
size="small"
56+
onClick={onRebuildIndex}
57+
disabled={loading === "rebuild"}
58+
>
59+
{loading === "rebuild" ? "Rebuilding..." : "Rebuild Index"}
60+
</Button>
61+
</div>
62+
)}
63+
</div>
64+
</section>
65+
);
66+
}

0 commit comments

Comments
 (0)