feat(webui): add first-run AI setup flow

This commit is contained in:
Xubin Ren
2026-09-02 16:25:07 +08:00
parent 042f96f6ba
commit b47f0980f1
15 changed files with 401 additions and 39 deletions
+27 -5
View File
@@ -70,6 +70,10 @@ import {
ModelPresetBadge,
type ModelPresetOption,
} from "@/components/thread/ModelPresetBadge";
import {
ModelSetupDialog,
type ModelSetupAvailability,
} from "@/components/thread/ModelSetupDialog";
import {
ACCEPT_ATTR,
MAX_ATTACHMENTS_PER_MESSAGE,
@@ -298,6 +302,7 @@ interface ThreadComposerProps {
modelProvider?: string | null;
modelProviderLabel?: string | null;
modelNeedsSetup?: boolean;
modelSetupAvailability?: ModelSetupAvailability;
fallbackModelName?: string | null;
onModelBadgeClick?: () => void;
onManageModels?: () => void;
@@ -997,6 +1002,7 @@ export function ThreadComposer({
modelProvider = null,
modelProviderLabel = null,
modelNeedsSetup = false,
modelSetupAvailability = { account: false, apiKey: false, local: false },
fallbackModelName = null,
onModelBadgeClick,
onManageModels,
@@ -1036,6 +1042,7 @@ export function ThreadComposer({
} | null>(null);
const [inlineError, setInlineError] = useState<string | null>(null);
const [sendPending, setSendPending] = useState(false);
const [modelSetupOpen, setModelSetupOpen] = useState(false);
const interactionDisabled = !!disabled || sendPending;
const [voiceErrorFading, setVoiceErrorFading] = useState(false);
const [slashMenuDismissed, setSlashMenuDismissed] = useState(false);
@@ -2008,7 +2015,7 @@ export function ThreadComposer({
const submit = useCallback(() => {
if (modelNeedsSetup) {
onModelBadgeClick?.();
setModelSetupOpen(true);
return;
}
if (!canSend) return;
@@ -2116,7 +2123,6 @@ export function ThreadComposer({
isStreaming,
maxTextBytes,
modelNeedsSetup,
onModelBadgeClick,
onSend,
onStop,
onQuotedContextChange,
@@ -2127,6 +2133,15 @@ export function ThreadComposer({
value,
]);
const openModelSetup = useCallback(() => {
setModelSetupOpen(true);
}, []);
const continueModelSetup = useCallback(() => {
setModelSetupOpen(false);
onModelBadgeClick?.();
}, [onModelBadgeClick]);
const onKeyDown = (e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
if (showCliAppMenu) {
if (e.key === "ArrowDown") {
@@ -2548,7 +2563,7 @@ export function ThreadComposer({
needsSetup={modelNeedsSetup}
fallbackModelName={fallbackModelName}
isHero={isHero}
onClick={modelNeedsSetup ? onModelBadgeClick : undefined}
onClick={modelNeedsSetup ? openModelSetup : undefined}
/>
) : null}
{!voiceRecorder.isRecording ? <ComposerContextBadge usage={contextUsage} /> : null}
@@ -2607,10 +2622,10 @@ export function ThreadComposer({
showStopButton
? t("thread.composer.stop")
: modelNeedsSetup
? t("thread.composer.configureModel", { defaultValue: "Configure model" })
? t("thread.composer.openModelSetup", { defaultValue: "Open AI setup" })
: t("thread.composer.send")
}
onClick={showStopButton ? handleStop : modelNeedsSetup ? onModelBadgeClick : undefined}
onClick={showStopButton ? handleStop : modelNeedsSetup ? openModelSetup : undefined}
className={cn(
"thread-composer-action touch-target rounded-full transition-transform",
showStopButton
@@ -2656,6 +2671,13 @@ export function ThreadComposer({
</div>
) : null}
</div>
<ModelSetupDialog
availability={modelSetupAvailability}
open={modelSetupOpen}
onOpenChange={setModelSetupOpen}
onReturnFocus={() => textareaRef.current?.focus()}
onSelect={continueModelSetup}
/>
</form>
);
}