This commit is contained in:
MarkLo 2025-11-23 10:20:05 +08:00
parent c0939063aa
commit 58521527fd
1 changed files with 258 additions and 123 deletions

View File

@ -28,23 +28,43 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
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"),
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),
shallow_thinking_agent: z.string().min(1, "請選擇快速思維模型"),
shallow_thinking_agent: z.string().min(1, "請選擇快速思維模型"),
deep_thinking_agent: z.string().min(1, "請選擇深層思維模型"),
// API Configuration
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_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_base_url: z
.string()
.url("請輸入有效的 URL")
.optional()
.or(z.literal("")),
embedding_api_key: z.string().optional().or(z.literal("")),
alpha_vantage_api_key: z.string().min(1, "請輸入 Alpha Vantage API Key"),
});
@ -87,7 +107,10 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
if (currentAnalysts.length === ANALYSTS.length) {
form.setValue("analysts", []);
} else {
form.setValue("analysts", ANALYSTS.map(a => a.value));
form.setValue(
"analysts",
ANALYSTS.map((a) => a.value)
);
}
};
@ -102,19 +125,26 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<Card className="shadow-lg">
<CardContent className="pt-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-6"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 分析師選擇區塊 - 全寬 */}
<div className="md:col-span-2 border-b pb-6">
<div className="flex justify-between items-center mb-4">
<FormLabel className="text-base font-semibold"></FormLabel>
<FormLabel className="text-base font-semibold">
</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={toggleSelectAll}
>
{form.watch("analysts").length === ANALYSTS.length ? "取消全選" : "全選"}
{form.watch("analysts").length === ANALYSTS.length
? "取消全選"
: "全選"}
</Button>
</div>
<FormField
@ -136,13 +166,19 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
>
<FormControl>
<Checkbox
checked={field.value?.includes(analyst.value)}
checked={field.value?.includes(
analyst.value
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, analyst.value])
? field.onChange([
...field.value,
analyst.value,
])
: field.onChange(
field.value?.filter(
(value: string) => value !== analyst.value
(value: string) =>
value !== analyst.value
)
);
}}
@ -191,9 +227,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormDescription>
</FormDescription>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
@ -209,7 +243,9 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormItem>
<FormLabel></FormLabel>
<Select
onValueChange={(value) => field.onChange(parseInt(value))}
onValueChange={(value) =>
field.onChange(parseInt(value))
}
defaultValue={field.value.toString()}
>
<FormControl>
@ -218,14 +254,18 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</SelectTrigger>
</FormControl>
<SelectContent className="max-h-80">
<SelectItem value="1" className="py-3 cursor-pointer"> - </SelectItem>
<SelectItem value="3" className="py-3 cursor-pointer"> - </SelectItem>
<SelectItem value="5" className="py-3 cursor-pointer"> - </SelectItem>
<SelectItem value="1" className="py-3 cursor-pointer">
-
</SelectItem>
<SelectItem value="3" className="py-3 cursor-pointer">
-
</SelectItem>
<SelectItem value="5" className="py-3 cursor-pointer">
-
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
</FormDescription>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
@ -237,8 +277,8 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select
onValueChange={field.onChange}
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
@ -248,42 +288,75 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</FormControl>
<SelectContent>
{/* OpenAI */}
<SelectItem value="gpt-5.1-2025-11-13">OpenAI: GPT-5.1</SelectItem>
<SelectItem value="gpt-5-mini-2025-08-07">OpenAI: GPT-5 Mini</SelectItem>
<SelectItem value="gpt-5-nano-2025-08-07">OpenAI: GPT-5 Nano</SelectItem>
<SelectItem value="gpt-4.1-mini">OpenAI: GPT-4.1 Mini</SelectItem>
<SelectItem value="gpt-4.1-nano">OpenAI: GPT-4.1 Nano</SelectItem>
<SelectItem value="o4-mini-2025-04-16">OpenAI: o4-mini</SelectItem>
{/* Anthropic */}
<SelectItem value="claude-haiku-4-5-20251001">Anthropic: Claude Haiku 4.5</SelectItem>
<SelectItem value="claude-sonnet-4-5-20250929">Anthropic: Claude Sonnet 4.5</SelectItem>
<SelectItem value="claude-sonnet-4-0">Anthropic: Claude Sonnet 4</SelectItem>
<SelectItem value="claude-3-5-haiku-20241022">Anthropic: Claude 3.5 Haiku</SelectItem>
<SelectItem value="claude-3-haiku-20240307">Anthropic: Claude 3 Haiku</SelectItem>
<SelectItem value="gpt-5.1-2025-11-13">
OpenAI: GPT-5.1
</SelectItem>
<SelectItem value="gpt-5-mini-2025-08-07">
OpenAI: GPT-5 Mini
</SelectItem>
<SelectItem value="gpt-5-nano-2025-08-07">
OpenAI: GPT-5 Nano
</SelectItem>
<SelectItem value="gpt-4.1-mini">
OpenAI: GPT-4.1 Mini
</SelectItem>
<SelectItem value="gpt-4.1-nano">
OpenAI: GPT-4.1 Nano
</SelectItem>
<SelectItem value="o4-mini-2025-04-16">
OpenAI: o4-mini
</SelectItem>
{/* Anthropic */}
<SelectItem value="claude-haiku-4-5-20251001">
Anthropic: Claude Haiku 4.5
</SelectItem>
<SelectItem value="claude-sonnet-4-5-20250929">
Anthropic: Claude Sonnet 4.5
</SelectItem>
<SelectItem value="claude-sonnet-4-0">
Anthropic: Claude Sonnet 4
</SelectItem>
<SelectItem value="claude-3-5-haiku-20241022">
Anthropic: Claude 3.5 Haiku
</SelectItem>
<SelectItem value="claude-3-haiku-20240307">
Anthropic: Claude 3 Haiku
</SelectItem>
{/* Grok */}
<SelectItem value="grok-4-1-fast-reasoning">Grok: 4.1 Fast Reasoning</SelectItem>
<SelectItem value="grok-4-1-fast-non-reasoning">Grok: 4.1 Fast Non Reasoning</SelectItem>
<SelectItem value="grok-4-fast-reasoning">Grok: 4 Fast Reasoning</SelectItem>
<SelectItem value="grok-4-fast-non-reasoning">Grok: 4 Fast Non Reasoning</SelectItem>
<SelectItem value="grok-4-1-fast-reasoning">
Grok: 4.1 Fast Reasoning
</SelectItem>
<SelectItem value="grok-4-1-fast-non-reasoning">
Grok: 4.1 Fast Non Reasoning
</SelectItem>
<SelectItem value="grok-4-fast-reasoning">
Grok: 4 Fast Reasoning
</SelectItem>
<SelectItem value="grok-4-fast-non-reasoning">
Grok: 4 Fast Non Reasoning
</SelectItem>
<SelectItem value="grok-4-0709">Grok: 4</SelectItem>
<SelectItem value="grok-3">Grok: 3</SelectItem>
<SelectItem value="grok-3-mini">Grok: 3 Mini</SelectItem>
<SelectItem value="grok-3-mini">
Grok: 3 Mini
</SelectItem>
{/* DeepSeek */}
<SelectItem value="deepseek-reasoner">DeepSeek: Reasoner</SelectItem>
<SelectItem value="deepseek-chat">DeepSeek: Chat</SelectItem>
<SelectItem value="deepseek-reasoner">
DeepSeek: Reasoner
</SelectItem>
<SelectItem value="deepseek-chat">
DeepSeek: Chat
</SelectItem>
{/* Qwen */}
<SelectItem value="qwen3-max">Qwen: 3 Max</SelectItem>
<SelectItem value="qwen-plus">Qwen: Plus</SelectItem>
</SelectContent>
</Select>
<FormDescription>
</FormDescription>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
@ -295,7 +368,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
@ -306,42 +379,75 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</FormControl>
<SelectContent>
{/* OpenAI */}
<SelectItem value="gpt-5.1-2025-11-13">OpenAI: GPT-5.1</SelectItem>
<SelectItem value="gpt-5-mini-2025-08-07">OpenAI: GPT-5 Mini</SelectItem>
<SelectItem value="gpt-5-nano-2025-08-07">OpenAI: GPT-5 Nano</SelectItem>
<SelectItem value="gpt-4.1-mini">OpenAI: GPT-4.1 Mini</SelectItem>
<SelectItem value="gpt-4.1-nano">OpenAI: GPT-4.1 Nano</SelectItem>
<SelectItem value="o4-mini-2025-04-16">OpenAI: o4-mini</SelectItem>
{/* Anthropic */}
<SelectItem value="claude-haiku-4-5-20251001">Anthropic: Claude Haiku 4.5</SelectItem>
<SelectItem value="claude-sonnet-4-5-20250929">Anthropic: Claude Sonnet 4.5</SelectItem>
<SelectItem value="claude-sonnet-4-0">Anthropic: Claude Sonnet 4</SelectItem>
<SelectItem value="claude-3-5-haiku-20241022">Anthropic: Claude 3.5 Haiku</SelectItem>
<SelectItem value="claude-3-haiku-20240307">Anthropic: Claude 3 Haiku</SelectItem>
<SelectItem value="gpt-5.1-2025-11-13">
OpenAI: GPT-5.1
</SelectItem>
<SelectItem value="gpt-5-mini-2025-08-07">
OpenAI: GPT-5 Mini
</SelectItem>
<SelectItem value="gpt-5-nano-2025-08-07">
OpenAI: GPT-5 Nano
</SelectItem>
<SelectItem value="gpt-4.1-mini">
OpenAI: GPT-4.1 Mini
</SelectItem>
<SelectItem value="gpt-4.1-nano">
OpenAI: GPT-4.1 Nano
</SelectItem>
<SelectItem value="o4-mini-2025-04-16">
OpenAI: o4-mini
</SelectItem>
{/* Anthropic */}
<SelectItem value="claude-haiku-4-5-20251001">
Anthropic: Claude Haiku 4.5
</SelectItem>
<SelectItem value="claude-sonnet-4-5-20250929">
Anthropic: Claude Sonnet 4.5
</SelectItem>
<SelectItem value="claude-sonnet-4-0">
Anthropic: Claude Sonnet 4
</SelectItem>
<SelectItem value="claude-3-5-haiku-20241022">
Anthropic: Claude 3.5 Haiku
</SelectItem>
<SelectItem value="claude-3-haiku-20240307">
Anthropic: Claude 3 Haiku
</SelectItem>
{/* Grok */}
<SelectItem value="grok-4-1-fast-reasoning">Grok: 4.1 Fast Reasoning</SelectItem>
<SelectItem value="grok-4-1-fast-non-reasoning">Grok: 4.1 Fast Non Reasoning</SelectItem>
<SelectItem value="grok-4-fast-reasoning">Grok: 4 Fast Reasoning</SelectItem>
<SelectItem value="grok-4-fast-non-reasoning">Grok: 4 Fast Non Reasoning</SelectItem>
<SelectItem value="grok-4-1-fast-reasoning">
Grok: 4.1 Fast Reasoning
</SelectItem>
<SelectItem value="grok-4-1-fast-non-reasoning">
Grok: 4.1 Fast Non Reasoning
</SelectItem>
<SelectItem value="grok-4-fast-reasoning">
Grok: 4 Fast Reasoning
</SelectItem>
<SelectItem value="grok-4-fast-non-reasoning">
Grok: 4 Fast Non Reasoning
</SelectItem>
<SelectItem value="grok-4-0709">Grok: 4</SelectItem>
<SelectItem value="grok-3">Grok: 3</SelectItem>
<SelectItem value="grok-3-mini">Grok: 3 Mini</SelectItem>
<SelectItem value="grok-3-mini">
Grok: 3 Mini
</SelectItem>
{/* DeepSeek */}
<SelectItem value="deepseek-reasoner">DeepSeek: Reasoner</SelectItem>
<SelectItem value="deepseek-chat">DeepSeek: Chat</SelectItem>
<SelectItem value="deepseek-reasoner">
DeepSeek: Reasoner
</SelectItem>
<SelectItem value="deepseek-chat">
DeepSeek: Chat
</SelectItem>
{/* Qwen */}
<SelectItem value="qwen3-max">Qwen: 3 Max</SelectItem>
<SelectItem value="qwen-plus">Qwen: Plus</SelectItem>
</SelectContent>
</Select>
<FormDescription>
</FormDescription>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
@ -349,10 +455,9 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</div>
</div>
{/* API Configuration Section */}
<div className="space-y-4 border-t pt-6 mt-6">
<h3 className="text-lg font-semibold">API </h3>
{/* API Configuration Section */}
<div className="space-y-4 border-t pt-6 mt-6">
<h3 className="text-lg font-semibold">API </h3>
<FormField
control={form.control}
@ -372,12 +477,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
[
"https://api.openai.com/v1",
"https://api.anthropic.com/",
"https://generativelanguage.googleapis.com/v1",
"https://api.x.ai/v1",
"https://api.deepseek.com",
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
].includes(field.value || "")
? field.value
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
].includes(field.value || "")
? field.value
: "custom"
}
>
@ -387,35 +491,45 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="https://api.openai.com/v1">OpenAI ()</SelectItem>
<SelectItem value="https://api.anthropic.com/">Anthropic</SelectItem>
<SelectItem value="https://generativelanguage.googleapis.com/v1">Google Gemini</SelectItem>
<SelectItem value="https://api.x.ai/v1">Grok (xAI)</SelectItem>
<SelectItem value="https://api.deepseek.com">DeepSeek</SelectItem>
<SelectItem value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1">Qwen (Alibaba)</SelectItem>
<SelectItem value="https://api.openai.com/v1">
OpenAI ()
</SelectItem>
<SelectItem value="https://api.anthropic.com/">
Anthropic
</SelectItem>
<SelectItem value="https://api.x.ai/v1">
Grok (xAI)
</SelectItem>
<SelectItem value="https://api.deepseek.com">
DeepSeek
</SelectItem>
<SelectItem value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1">
Qwen (Alibaba)
</SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
{(![
"https://api.openai.com/v1",
"https://api.anthropic.com/",
"https://generativelanguage.googleapis.com/v1",
"https://api.x.ai/v1",
"https://api.deepseek.com",
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
].includes(field.value || "") || field.value === "") && (
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
].includes(field.value || "") ||
field.value === "") && (
<div className="mt-2">
<FormControl>
<Input
placeholder="請輸入自訂 Base URL"
value={field.value || ""}
onChange={field.onChange}
<Input
placeholder="請輸入自訂 Base URL"
value={field.value || ""}
onChange={field.onChange}
/>
</FormControl>
</div>
)}
<FormDescription>
API
</FormDescription>
@ -459,12 +573,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
[
"https://api.openai.com/v1",
"https://api.anthropic.com/",
"https://generativelanguage.googleapis.com/v1",
"https://api.x.ai/v1",
"https://api.deepseek.com",
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
].includes(field.value || "")
? field.value
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
].includes(field.value || "")
? field.value
: "custom"
}
>
@ -474,35 +587,45 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="https://api.openai.com/v1">OpenAI ()</SelectItem>
<SelectItem value="https://api.anthropic.com/">Anthropic</SelectItem>
<SelectItem value="https://generativelanguage.googleapis.com/v1">Google Gemini</SelectItem>
<SelectItem value="https://api.x.ai/v1">Grok (xAI)</SelectItem>
<SelectItem value="https://api.deepseek.com">DeepSeek</SelectItem>
<SelectItem value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1">Qwen (Alibaba)</SelectItem>
<SelectItem value="https://api.openai.com/v1">
OpenAI ()
</SelectItem>
<SelectItem value="https://api.anthropic.com/">
Anthropic
</SelectItem>
<SelectItem value="https://api.x.ai/v1">
Grok (xAI)
</SelectItem>
<SelectItem value="https://api.deepseek.com">
DeepSeek
</SelectItem>
<SelectItem value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1">
Qwen (Alibaba)
</SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
{(![
"https://api.openai.com/v1",
"https://api.anthropic.com/",
"https://generativelanguage.googleapis.com/v1",
"https://api.x.ai/v1",
"https://api.deepseek.com",
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
].includes(field.value || "") || field.value === "") && (
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
].includes(field.value || "") ||
field.value === "") && (
<div className="mt-2">
<FormControl>
<Input
placeholder="請輸入自訂 Base URL"
value={field.value || ""}
onChange={field.onChange}
<Input
placeholder="請輸入自訂 Base URL"
value={field.value || ""}
onChange={field.onChange}
/>
</FormControl>
</div>
)}
<FormDescription>
API
</FormDescription>
@ -543,7 +666,8 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
}
}}
defaultValue={
field.value === "https://api.openai.com/v1" || !field.value
field.value === "https://api.openai.com/v1" ||
!field.value
? "https://api.openai.com/v1"
: "custom"
}
@ -554,23 +678,25 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="https://api.openai.com/v1">OpenAI ()</SelectItem>
<SelectItem value="https://api.openai.com/v1">
OpenAI ()
</SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
{field.value !== "https://api.openai.com/v1" && (
<div className="mt-2">
<FormControl>
<Input
placeholder="請輸入自訂 Base URL"
value={field.value || ""}
onChange={field.onChange}
<Input
placeholder="請輸入自訂 Base URL"
value={field.value || ""}
onChange={field.onChange}
/>
</FormControl>
</div>
)}
<FormDescription>
API
</FormDescription>
@ -603,7 +729,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormItem>
<FormLabel>Alpha Vantage API Key *</FormLabel>
<FormControl>
<Input type="password" placeholder="輸入 Alpha Vantage API Key必填" {...field} />
<Input
type="password"
placeholder="輸入 Alpha Vantage API Key必填"
{...field}
/>
</FormControl>
<FormDescription>
@ -614,7 +744,12 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
/>
</div>
<Button type="submit" className="w-full" disabled={loading} size="lg">
<Button
type="submit"
className="w-full"
disabled={loading}
size="lg"
>
{loading ? "執行分析中..." : "執行分析"}
</Button>
</form>