|
9 | 9 | CloudIcon, |
10 | 10 | EyeIcon, |
11 | 11 | EyeOffIcon, |
| 12 | + FileIcon, |
12 | 13 | GlobeIcon, |
13 | 14 | LoaderIcon, |
14 | 15 | SparklesIcon, |
@@ -41,6 +42,7 @@ import { |
41 | 42 | } from "@/components/ui/sheet"; |
42 | 43 | import { Skeleton } from "@/components/ui/skeleton"; |
43 | 44 | import { Switch } from "@/components/ui/switch"; |
| 45 | +import { Textarea } from "@/components/ui/textarea"; |
44 | 46 | // Tabs removed - consolidated into single pane |
45 | 47 | import { getSearchDomainUrl, searchDomains } from "@/lib/ai/search-domains"; |
46 | 48 | import { guestRegex } from "@/lib/constants"; |
@@ -88,6 +90,7 @@ async function fetchSettings() { |
88 | 90 | timezone: string; |
89 | 91 | searchDomainIds: string[]; |
90 | 92 | maxIterations: string; |
| 93 | + customInstructions: string | null; |
91 | 94 | }; |
92 | 95 | } catch (error) { |
93 | 96 | console.error("[Settings] Error in fetchSettings:", error); |
@@ -260,6 +263,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { |
260 | 263 | const [searchDomainIds, setSearchDomainIds] = useState<string[]>([]); |
261 | 264 | const [maxIterations, setMaxIterations] = useState("10"); |
262 | 265 | const maxIterationsId = useId(); |
| 266 | + const [customInstructions, setCustomInstructions] = useState(""); |
| 267 | + const customInstructionsFileInputId = useId(); |
| 268 | + const customInstructionsTextareaId = useId(); |
263 | 269 | const [isConnectingNetSuite, setIsConnectingNetSuite] = useState(false); |
264 | 270 | const searchInputRef = useRef<HTMLInputElement>(null); |
265 | 271 | const initializedForThisOpenRef = useRef(false); |
@@ -330,6 +336,10 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { |
330 | 336 | setTimezone(settings.timezone ?? "UTC"); |
331 | 337 | setSearchDomainIds(settings.searchDomainIds ?? []); |
332 | 338 | setMaxIterations(settings.maxIterations ?? "10"); |
| 339 | + setCustomInstructions( |
| 340 | + (settings as { customInstructions?: string | null }) |
| 341 | + ?.customInstructions ?? "", |
| 342 | + ); |
333 | 343 | initializedForThisOpenRef.current = true; |
334 | 344 | } else { |
335 | 345 | console.warn("[Settings] Settings object invalid:", settings); |
@@ -363,6 +373,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { |
363 | 373 | timezone: timezone?.trim() || "UTC", |
364 | 374 | searchDomainIds: effectiveSearchDomainIds, |
365 | 375 | maxIterations: maxIterations?.trim() || "10", |
| 376 | + customInstructions: customInstructions?.trim() || null, |
366 | 377 | }; |
367 | 378 |
|
368 | 379 | const response = await fetch("/api/settings", { |
@@ -1068,6 +1079,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { |
1068 | 1079 | <div |
1069 | 1080 | className={cn( |
1070 | 1081 | "flex", |
| 1082 | + "shrink-0", |
1071 | 1083 | "items-center", |
1072 | 1084 | "justify-center", |
1073 | 1085 | "rounded-md", |
@@ -1143,6 +1155,111 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { |
1143 | 1155 | )} |
1144 | 1156 | </CardContent> |
1145 | 1157 | </Card> |
| 1158 | + |
| 1159 | + {/* Custom Instructions */} |
| 1160 | + <Card className="bg-background shadow-none"> |
| 1161 | + <CardHeader className="py-6"> |
| 1162 | + <div className="flex items-start justify-between gap-3"> |
| 1163 | + <div className="flex items-start gap-3"> |
| 1164 | + <div |
| 1165 | + className={cn( |
| 1166 | + "flex", |
| 1167 | + "shrink-0", |
| 1168 | + "items-center", |
| 1169 | + "justify-center", |
| 1170 | + "rounded-md", |
| 1171 | + "bg-muted", |
| 1172 | + "text-primary", |
| 1173 | + "size-10", |
| 1174 | + "mt-0.5", |
| 1175 | + )} |
| 1176 | + > |
| 1177 | + <FileIcon size={20} /> |
| 1178 | + </div> |
| 1179 | + <div> |
| 1180 | + <CardTitle className="text-base"> |
| 1181 | + Custom Instructions |
| 1182 | + </CardTitle> |
| 1183 | + <p className="text-muted-foreground text-sm"> |
| 1184 | + Add your own instructions (e.g. from{" "} |
| 1185 | + <code className="rounded bg-muted px-1 py-0.5 text-xs"> |
| 1186 | + instructions.md |
| 1187 | + </code> |
| 1188 | + ) to tailor how Ava responds. These are appended to |
| 1189 | + the system prompt as additional user instructions. |
| 1190 | + </p> |
| 1191 | + </div> |
| 1192 | + </div> |
| 1193 | + </div> |
| 1194 | + </CardHeader> |
| 1195 | + <CardContent className="pt-0"> |
| 1196 | + {showSkeletons ? ( |
| 1197 | + <Skeleton className="h-32 w-full" /> |
| 1198 | + ) : ( |
| 1199 | + <div className="space-y-2"> |
| 1200 | + <div className="flex items-center gap-2"> |
| 1201 | + <Label |
| 1202 | + className="sr-only" |
| 1203 | + htmlFor={customInstructionsFileInputId} |
| 1204 | + > |
| 1205 | + Import instructions.md file |
| 1206 | + </Label> |
| 1207 | + <Input |
| 1208 | + accept=".md" |
| 1209 | + aria-label="Import instructions.md file" |
| 1210 | + className="hidden" |
| 1211 | + id={customInstructionsFileInputId} |
| 1212 | + onChange={(e) => { |
| 1213 | + const file = e.target.files?.[0]; |
| 1214 | + if (!file) return; |
| 1215 | + const reader = new FileReader(); |
| 1216 | + reader.onload = () => { |
| 1217 | + const text = reader.result; |
| 1218 | + if (typeof text === "string") { |
| 1219 | + setCustomInstructions(text); |
| 1220 | + toast({ |
| 1221 | + type: "success", |
| 1222 | + description: `Imported ${file.name}`, |
| 1223 | + }); |
| 1224 | + } |
| 1225 | + }; |
| 1226 | + reader.readAsText(file, "UTF-8"); |
| 1227 | + e.target.value = ""; |
| 1228 | + }} |
| 1229 | + type="file" |
| 1230 | + /> |
| 1231 | + <Button |
| 1232 | + onClick={() => |
| 1233 | + document |
| 1234 | + .getElementById(customInstructionsFileInputId) |
| 1235 | + ?.click() |
| 1236 | + } |
| 1237 | + type="button" |
| 1238 | + variant="outline" |
| 1239 | + > |
| 1240 | + Import instructions.md |
| 1241 | + </Button> |
| 1242 | + </div> |
| 1243 | + <Label |
| 1244 | + className="sr-only" |
| 1245 | + htmlFor={customInstructionsTextareaId} |
| 1246 | + > |
| 1247 | + Custom instructions content |
| 1248 | + </Label> |
| 1249 | + <Textarea |
| 1250 | + aria-label="Custom instructions content" |
| 1251 | + className="min-h-[120px] font-mono text-sm" |
| 1252 | + id={customInstructionsTextareaId} |
| 1253 | + onChange={(e) => |
| 1254 | + setCustomInstructions(e.target.value) |
| 1255 | + } |
| 1256 | + placeholder="Paste or import your custom instructions (Markdown supported)..." |
| 1257 | + value={customInstructions} |
| 1258 | + /> |
| 1259 | + </div> |
| 1260 | + )} |
| 1261 | + </CardContent> |
| 1262 | + </Card> |
1146 | 1263 | </> |
1147 | 1264 | )} |
1148 | 1265 |
|
|
0 commit comments