/** * Analysis form component */ "use client"; import { useState, useEffect, useMemo } from "react"; import { useForm, ControllerRenderProps } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { format } from "date-fns"; import { CheckIcon } from "lucide-react"; import { getApiSettingsAsync } from "@/lib/storage"; import { getBaseUrlForModel, getApiKeyForModel } from "@/lib/api-helpers"; import Image from "next/image"; import { useLanguage } from "@/contexts/LanguageContext"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { DatePicker } from "@/components/ui/date-picker"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import type { AnalysisRequest } from "@/lib/types"; const formSchema = z.object({ ticker: z.string().min(1, "股票代碼為必填").max(10), analysis_date: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, "日期格式必須為 YYYY-MM-DD"), analysts: z.array(z.string()).min(1, "請至少選擇一位分析師"), research_depth: z.number().int().min(1).max(5), analysis_mode: z.enum(["fast", "deep"]).default("deep"), quick_think_llm: z.string().min(1, "請選擇快速思維模型"), deep_think_llm: z.string().min(1, "請選擇深層思維模型"), embedding_model: z.string().min(1, "請選擇嵌入式模型"), // Market type selection: us=美股, twse=上市, tpex=上櫃/興櫃 market_type: z.enum(["us", "twse", "tpex"]), // Custom model names (when "custom" is selected) custom_quick_think_model: z.string().optional(), custom_deep_think_model: z.string().optional(), // API Configuration (hidden from UI, populated from localStorage) quick_think_base_url: z .string() .url("請輸入有效的 URL") .optional() .or(z.literal("")), deep_think_base_url: z .string() .url("請輸入有效的 URL") .optional() .or(z.literal("")), quick_think_api_key: z.string().optional().or(z.literal("")), deep_think_api_key: z.string().optional().or(z.literal("")), embedding_base_url: z .string() .url("請輸入有效的 URL") .optional() .or(z.literal("")), embedding_api_key: z.string().optional().or(z.literal("")), // 本地模型不需要 API Key alpha_vantage_api_key: z.string().optional().or(z.literal("")), // 選填 finmind_api_key: z.string().optional().or(z.literal("")), // 選填 }); interface AnalysisFormProps { onSubmit: (data: AnalysisRequest) => void; loading?: boolean; } // ANALYSTS is now defined inside the component using translations export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { const { t, locale } = useLanguage(); // Define ANALYSTS using translations const ANALYSTS = useMemo(() => [ { value: "market", label: t.agents.market_analyst }, { value: "social", label: t.agents.social_analyst }, { value: "news", label: t.agents.news_analyst }, { value: "fundamentals", label: t.agents.fundamentals_analyst }, ], [t]); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { ticker: "NVDA", analysis_date: format(new Date(), "yyyy-MM-dd"), analysts: ["market", "social", "news", "fundamentals"], // 預設全選 research_depth: 3, // 預設中等層級 analysis_mode: "deep", // 預設深層分析 market_type: "us", // 預設美股 quick_think_llm: "gpt-5-mini", deep_think_llm: "gpt-5-mini", embedding_model: "all-MiniLM-L6-v2", // 預設使用本地開源模型 custom_quick_think_model: "", custom_deep_think_model: "", quick_think_base_url: "https://api.openai.com/v1", deep_think_base_url: "https://api.openai.com/v1", quick_think_api_key: "", deep_think_api_key: "", embedding_base_url: "https://api.openai.com/v1", embedding_api_key: "", alpha_vantage_api_key: "", finmind_api_key: "", }, }); // Load API settings from localStorage and update when models change const quickThinkLlm = form.watch("quick_think_llm"); const deepThinkLlm = form.watch("deep_think_llm"); const embeddingModel = form.watch("embedding_model"); const marketType = form.watch("market_type"); const isQuickThinkCustom = quickThinkLlm === "custom"; const isDeepThinkCustom = deepThinkLlm === "custom"; const isLocalEmbedding = ["all-MiniLM-L6-v2", "all-mpnet-base-v2"].includes(embeddingModel); useEffect(() => { // Use async version to get decrypted API keys const loadSettings = async () => { const savedSettings = await getApiSettingsAsync(); // For custom models, always use custom base URL and API key if (isQuickThinkCustom) { form.setValue( "quick_think_base_url", savedSettings.custom_base_url || "" ); form.setValue( "quick_think_api_key", savedSettings.custom_api_key || "" ); } else { form.setValue( "quick_think_base_url", getBaseUrlForModel(quickThinkLlm, savedSettings.custom_base_url) ); form.setValue( "quick_think_api_key", getApiKeyForModel(quickThinkLlm, savedSettings) ); } if (isDeepThinkCustom) { form.setValue( "deep_think_base_url", savedSettings.custom_base_url || "" ); form.setValue("deep_think_api_key", savedSettings.custom_api_key || ""); } else { form.setValue( "deep_think_base_url", getBaseUrlForModel(deepThinkLlm, savedSettings.custom_base_url) ); form.setValue( "deep_think_api_key", getApiKeyForModel(deepThinkLlm, savedSettings) ); } // 本地模型不需要設定 API Key 和 Base URL if (!isLocalEmbedding) { form.setValue( "embedding_base_url", savedSettings.custom_base_url || "https://api.openai.com/v1" ); form.setValue( "embedding_api_key", savedSettings.custom_api_key || savedSettings.openai_api_key ); } else { form.setValue("embedding_base_url", ""); form.setValue("embedding_api_key", ""); } form.setValue( "alpha_vantage_api_key", savedSettings.alpha_vantage_api_key || "" ); form.setValue("finmind_api_key", savedSettings.finmind_api_key || ""); }; loadSettings(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [quickThinkLlm, deepThinkLlm, embeddingModel, isQuickThinkCustom, isDeepThinkCustom, isLocalEmbedding]); // 當市場類型改變時,更新預設股票代碼和提示 useEffect(() => { const currentTicker = form.getValues("ticker"); // 只在用戶未修改預設值時才自動切換 const isTwStock = marketType === "twse" || marketType === "tpex"; const isDefaultUsTicker = currentTicker === "NVDA" || currentTicker === "AAPL"; const isDefaultTwTicker = currentTicker === "2330" || currentTicker === "2317" || currentTicker === "6488"; if (isTwStock && isDefaultUsTicker) { form.setValue("ticker", marketType === "twse" ? "2330" : "6488"); } else if (marketType === "us" && isDefaultTwTicker) { form.setValue("ticker", "NVDA"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [marketType]); // 全選/取消全選 const toggleSelectAll = () => { const currentAnalysts = form.getValues("analysts"); if (currentAnalysts.length === ANALYSTS.length) { form.setValue("analysts", []); } else { form.setValue( "analysts", ANALYSTS.map((a) => a.value) ); } }; function handleSubmit(values: z.infer) { // Use custom model names if "custom" is selected const finalQuickThinkLlm = values.quick_think_llm === "custom" ? values.custom_quick_think_model || "" : values.quick_think_llm; const finalDeepThinkLlm = values.deep_think_llm === "custom" ? values.custom_deep_think_model || "" : values.deep_think_llm; // Validate custom model names if ( values.quick_think_llm === "custom" && !values.custom_quick_think_model ) { form.setError("custom_quick_think_model", { type: "manual", message: "請輸入快速思維模型的完整名稱", }); return; } if (values.deep_think_llm === "custom" && !values.custom_deep_think_model) { form.setError("custom_deep_think_model", { type: "manual", message: "請輸入深層思維模型的完整名稱", }); return; } // Validate API keys are set (they come from localStorage/settings) if (!values.quick_think_api_key) { alert("請先在右上角「設定」中設定您的 API Key。\n\n快速思維模型需要對應的 API Key 才能運作。"); return; } if (!values.deep_think_api_key) { alert("請先在右上角「設定」中設定您的 API Key。\n\n深層思維模型需要對應的 API Key 才能運作。"); return; } const request: AnalysisRequest = { ...values, quick_think_llm: finalQuickThinkLlm, deep_think_llm: finalDeepThinkLlm, language: locale as "en" | "zh-TW", // Pass current UI language to backend }; onSubmit(request); } return (
{/* 分析師選擇區塊 - 全寬 */}
{t.form.analysts}
(
{ANALYSTS.map((analyst) => { const isSelected = field.value?.includes( analyst.value ); return (
{ const newValue = isSelected ? field.value?.filter( (v: string) => v !== analyst.value ) : [...(field.value ?? []), analyst.value]; field.onChange(newValue); }} className={cn( "relative flex cursor-pointer flex-row items-center gap-3 rounded-lg border-2 p-4 transition-all hover:bg-accent", isSelected ? "border-primary bg-primary/5 text-primary" : "border-muted-foreground/25 bg-card text-muted-foreground" )} >
{isSelected && ( )}
{analyst.label}
); })}
)} />
{/* 第一行:市場類型、股票代碼、分析日期(3列) */}
{/* 市場類型選擇 */} ( {t.form.marketType} {t.form.selectMarketDesc} )} /> {/* 股票代碼 */} ( {t.form.ticker} {marketType === "us" ? t.form.tickerDescUS : marketType === "twse" ? t.form.tickerDescTWSE : t.form.tickerDescTPEX} )} /> ( {t.form.analysisDate} { field.onChange( date ? format(date, "yyyy-MM-dd") : "" ); }} placeholder={t.form.selectDate} className="w-full" /> {t.form.selectDate} )} />
{/* 分析模式行 */}
( 分析模式 快速模式跳過辯論,深層模式包含投資和風險辯論 )} />
{/* 第二行:研究深度、快速思維模型、深層思維模型、嵌入式模型(4列) */}
( {t.form.researchDepth} {t.form.selectDepth} )} /> ( {t.form.quickThinkModel} {t.form.quickResponseModel} )} /> {/* Custom Quick Think Model Input */} {isQuickThinkCustom && ( ( 自訂快速思維模型名稱 請輸入完整的模型名稱(此模型將使用自訂端點) )} /> )} ( {t.form.deepThinkModel} {t.form.complexReasoningModel} )} /> {/* Custom Deep Think Model Input */} {isDeepThinkCustom && ( ( {t.form.customDeepThinkModelName} 請輸入完整的模型名稱(此模型將使用自訂端點) )} /> )} {/* 嵌入式模型 */} ( {t.form.embeddingModel} {isLocalEmbedding ? t.form.localModelNoApiKey : t.form.needsOpenAiApiKey} )} />
); }