This commit is contained in:
parent
5dee4b323c
commit
a0e4365fc3
|
|
@ -3,12 +3,14 @@
|
||||||
*/
|
*/
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
|
import { getApiSettings } from "@/lib/storage";
|
||||||
|
import { getBaseUrlForModel, getApiKeyForModel } from "@/lib/api-helpers";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -51,7 +53,7 @@ const formSchema = z.object({
|
||||||
quick_think_llm: z.string().min(1, "請選擇快速思維模型"),
|
quick_think_llm: z.string().min(1, "請選擇快速思維模型"),
|
||||||
deep_think_llm: z.string().min(1, "請選擇深層思維模型"),
|
deep_think_llm: z.string().min(1, "請選擇深層思維模型"),
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration (hidden from UI, populated from localStorage)
|
||||||
quick_think_base_url: z
|
quick_think_base_url: z
|
||||||
.string()
|
.string()
|
||||||
.url("請輸入有效的 URL")
|
.url("請輸入有效的 URL")
|
||||||
|
|
@ -105,6 +107,24 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load API settings from localStorage and update when models change
|
||||||
|
useEffect(() => {
|
||||||
|
const savedSettings = getApiSettings();
|
||||||
|
const quickThinkLlm = form.getValues("quick_think_llm");
|
||||||
|
const deepThinkLlm = form.getValues("deep_think_llm");
|
||||||
|
|
||||||
|
// Set base URLs based on selected models
|
||||||
|
form.setValue("quick_think_base_url", getBaseUrlForModel(quickThinkLlm));
|
||||||
|
form.setValue("deep_think_base_url", getBaseUrlForModel(deepThinkLlm));
|
||||||
|
form.setValue("embedding_base_url", "https://api.openai.com/v1");
|
||||||
|
|
||||||
|
// Set API keys based on selected models
|
||||||
|
form.setValue("quick_think_api_key", getApiKeyForModel(quickThinkLlm, savedSettings));
|
||||||
|
form.setValue("deep_think_api_key", getApiKeyForModel(deepThinkLlm, savedSettings));
|
||||||
|
form.setValue("embedding_api_key", savedSettings.openai_api_key);
|
||||||
|
form.setValue("alpha_vantage_api_key", savedSettings.alpha_vantage_api_key);
|
||||||
|
}, [form, form.watch("quick_think_llm"), form.watch("deep_think_llm")]);
|
||||||
|
|
||||||
// 全選/取消全選
|
// 全選/取消全選
|
||||||
const toggleSelectAll = () => {
|
const toggleSelectAll = () => {
|
||||||
const currentAnalysts = form.getValues("analysts");
|
const currentAnalysts = form.getValues("analysts");
|
||||||
|
|
@ -505,299 +525,6 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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}
|
|
||||||
name="quick_think_base_url"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>快速思維模型 Base URL</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value !== "custom") {
|
|
||||||
field.onChange(value);
|
|
||||||
} else {
|
|
||||||
field.onChange(""); // Clear value for custom input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
[
|
|
||||||
"https://api.openai.com/v1",
|
|
||||||
"https://api.anthropic.com",
|
|
||||||
"https://api.x.ai/v1",
|
|
||||||
"https://api.deepseek.com/v1",
|
|
||||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
||||||
].includes(field.value || "")
|
|
||||||
? field.value
|
|
||||||
: "custom"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="選擇 API 端點" />
|
|
||||||
</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/v1beta/openai">
|
|
||||||
Google (Gemini)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="https://api.x.ai/v1">
|
|
||||||
Grok (xAI)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="https://api.deepseek.com/v1">
|
|
||||||
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/v1beta/openai",
|
|
||||||
"https://api.x.ai/v1",
|
|
||||||
"https://api.deepseek.com/v1",
|
|
||||||
"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}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormDescription>
|
|
||||||
快速思維模型的 API 基礎網址
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="quick_think_api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>快速思維模型 API Key *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="sk-..." {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
該模型的專屬 API Key(必填)
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="deep_think_base_url"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>深層思維模型 Base URL</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value !== "custom") {
|
|
||||||
field.onChange(value);
|
|
||||||
} else {
|
|
||||||
field.onChange(""); // Clear value for custom input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
[
|
|
||||||
"https://api.openai.com/v1",
|
|
||||||
"https://api.anthropic.com",
|
|
||||||
"https://api.x.ai/v1",
|
|
||||||
"https://api.deepseek.com/v1",
|
|
||||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
||||||
].includes(field.value || "")
|
|
||||||
? field.value
|
|
||||||
: "custom"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="選擇 API 端點" />
|
|
||||||
</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/v1beta/openai">
|
|
||||||
Google (Gemini)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="https://api.x.ai/v1">
|
|
||||||
Grok (xAI)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="https://api.deepseek.com/v1">
|
|
||||||
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/v1beta/openai",
|
|
||||||
"https://api.x.ai/v1",
|
|
||||||
"https://api.deepseek.com/v1",
|
|
||||||
"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}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormDescription>
|
|
||||||
深層思維模型的 API 基礎網址
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="deep_think_api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>深層思維模型 API Key *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="sk-..." {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
該模型的專屬 API Key(必填)
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="embedding_base_url"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>嵌入模型 Base URL</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value !== "custom") {
|
|
||||||
field.onChange(value);
|
|
||||||
} else {
|
|
||||||
field.onChange(""); // Clear value for custom input
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
field.value === "https://api.openai.com/v1" ||
|
|
||||||
!field.value
|
|
||||||
? "https://api.openai.com/v1"
|
|
||||||
: "custom"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="選擇嵌入模型端點" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormDescription>
|
|
||||||
嵌入向量生成的 API 端點(用於記憶體系統)
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="embedding_api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>嵌入模型 API Key *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="sk-..." {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>該端點的 API Key(必填)</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="alpha_vantage_api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Alpha Vantage API Key *</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="輸入 Alpha Vantage API Key(必填)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
用於獲取市場基本面數據(必填)
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 hover:from-blue-600 hover:to-pink-600 dark:hover:from-blue-700 dark:hover:to-purple-700 shadow-lg hover:shadow-xl transition-all animate-heartbeat"
|
className="w-full bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 hover:from-blue-600 hover:to-pink-600 dark:hover:from-blue-700 dark:hover:to-purple-700 shadow-lg hover:shadow-xl transition-all animate-heartbeat"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ThemeToggle } from "@/components/theme/ThemeToggle";
|
import { ThemeToggle } from "@/components/theme/ThemeToggle";
|
||||||
|
import { ApiSettingsDialog } from "@/components/settings/ApiSettingsDialog";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -28,6 +29,7 @@ export function Header() {
|
||||||
>
|
>
|
||||||
分析
|
分析
|
||||||
</Link>
|
</Link>
|
||||||
|
<ApiSettingsDialog />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
/**
|
||||||
|
* API Settings Dialog Component
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Settings } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
getApiSettings,
|
||||||
|
saveApiSettings,
|
||||||
|
clearApiSettings,
|
||||||
|
type ApiSettings,
|
||||||
|
DEFAULT_API_SETTINGS,
|
||||||
|
} from "@/lib/storage";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
// Required
|
||||||
|
openai_api_key: z.string().min(1, "OpenAI API Key 為必填"),
|
||||||
|
alpha_vantage_api_key: z.string().min(1, "Alpha Vantage API Key 為必填"),
|
||||||
|
|
||||||
|
// Optional
|
||||||
|
anthropic_api_key: z.string().optional().or(z.literal("")),
|
||||||
|
google_api_key: z.string().optional().or(z.literal("")),
|
||||||
|
grok_api_key: z.string().optional().or(z.literal("")),
|
||||||
|
deepseek_api_key: z.string().optional().or(z.literal("")),
|
||||||
|
qwen_api_key: z.string().optional().or(z.literal("")),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export function ApiSettingsDialog() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: getApiSettings(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load settings when dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
const settings = getApiSettings();
|
||||||
|
form.reset(settings);
|
||||||
|
setSaveSuccess(false);
|
||||||
|
}
|
||||||
|
}, [open, form]);
|
||||||
|
|
||||||
|
const onSubmit = (values: FormValues) => {
|
||||||
|
try {
|
||||||
|
// Type assertion since our form values match ApiSettings structure
|
||||||
|
saveApiSettings(values as ApiSettings);
|
||||||
|
setSaveSuccess(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setSaveSuccess(false);
|
||||||
|
setOpen(false);
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save settings:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
clearApiSettings();
|
||||||
|
form.reset(DEFAULT_API_SETTINGS);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-white hover:bg-white/20"
|
||||||
|
title="API 設定"
|
||||||
|
>
|
||||||
|
<Settings className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>API 配置</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
設定您的 API 金鑰。這些資訊會儲存在瀏覽器的本機儲存空間中。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{/* Required Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-primary">必填項目</h3>
|
||||||
|
|
||||||
|
{/* OpenAI API Key */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="openai_api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>OpenAI API Key *</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="sk-..." {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
用於 OpenAI 模型(GPT-4, GPT-5, o4 等)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Alpha Vantage API Key */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="alpha_vantage_api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Alpha Vantage API Key *</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="輸入 Alpha Vantage API Key"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
用於獲取市場基本面數據(必填)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Optional Section */}
|
||||||
|
<div className="space-y-4 border-t pt-4">
|
||||||
|
<h3 className="text-lg font-semibold text-muted-foreground">
|
||||||
|
選填項目(依需求填寫)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Anthropic API Key */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="anthropic_api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Anthropic API Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="sk-..." {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>用於 Claude 模型</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Google API Key */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="google_api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Google API Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="..." {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>用於 Gemini 模型</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Grok API Key */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="grok_api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Grok (xAI) API Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="xai-..." {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>用於 Grok 模型</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* DeepSeek API Key */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="deepseek_api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>DeepSeek API Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="sk-..." {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>用於 DeepSeek 模型</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Qwen API Key */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="qwen_api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Qwen (Alibaba) API Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="sk-..." {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>用於 Qwen 模型</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{saveSuccess && (
|
||||||
|
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 text-green-800 dark:text-green-300 text-sm">
|
||||||
|
✓ 設定已成功儲存
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-4">
|
||||||
|
<Button type="submit" className="flex-1">
|
||||||
|
儲存設定
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
清除設定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/**
|
||||||
|
* Helper functions for API configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ApiSettings } from "./storage";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base URL for a given LLM model
|
||||||
|
*/
|
||||||
|
export function getBaseUrlForModel(model: string): string {
|
||||||
|
// OpenAI models
|
||||||
|
if (
|
||||||
|
model.startsWith("gpt-") ||
|
||||||
|
model.startsWith("o4-") ||
|
||||||
|
model.startsWith("o1-")
|
||||||
|
) {
|
||||||
|
return "https://api.openai.com/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anthropic models
|
||||||
|
if (model.startsWith("claude-")) {
|
||||||
|
return "https://api.anthropic.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google models
|
||||||
|
if (model.startsWith("gemini-")) {
|
||||||
|
return "https://generativelanguage.googleapis.com/v1beta/openai";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grok models
|
||||||
|
if (model.startsWith("grok-")) {
|
||||||
|
return "https://api.x.ai/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepSeek models
|
||||||
|
if (model.startsWith("deepseek-")) {
|
||||||
|
return "https://api.deepseek.com/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen models
|
||||||
|
if (model.startsWith("qwen")) {
|
||||||
|
return "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to OpenAI
|
||||||
|
return "https://api.openai.com/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the API key for a given LLM model from saved settings
|
||||||
|
*/
|
||||||
|
export function getApiKeyForModel(
|
||||||
|
model: string,
|
||||||
|
settings: ApiSettings
|
||||||
|
): string {
|
||||||
|
// OpenAI models
|
||||||
|
if (
|
||||||
|
model.startsWith("gpt-") ||
|
||||||
|
model.startsWith("o4-") ||
|
||||||
|
model.startsWith("o1-")
|
||||||
|
) {
|
||||||
|
return settings.openai_api_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anthropic models
|
||||||
|
if (model.startsWith("claude-")) {
|
||||||
|
return settings.anthropic_api_key || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google models
|
||||||
|
if (model.startsWith("gemini-")) {
|
||||||
|
return settings.google_api_key || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grok models
|
||||||
|
if (model.startsWith("grok-")) {
|
||||||
|
return settings.grok_api_key || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepSeek models
|
||||||
|
if (model.startsWith("deepseek-")) {
|
||||||
|
return settings.deepseek_api_key || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen models
|
||||||
|
if (model.startsWith("qwen")) {
|
||||||
|
return settings.qwen_api_key || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to OpenAI
|
||||||
|
return settings.openai_api_key;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* localStorage utility for API settings
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ApiSettings {
|
||||||
|
// Required providers
|
||||||
|
openai_api_key: string;
|
||||||
|
alpha_vantage_api_key: string;
|
||||||
|
|
||||||
|
// Optional providers
|
||||||
|
anthropic_api_key: string;
|
||||||
|
google_api_key: string;
|
||||||
|
grok_api_key: string;
|
||||||
|
deepseek_api_key: string;
|
||||||
|
qwen_api_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = "tradingagents_api_settings";
|
||||||
|
|
||||||
|
export const DEFAULT_API_SETTINGS: ApiSettings = {
|
||||||
|
openai_api_key: "",
|
||||||
|
alpha_vantage_api_key: "",
|
||||||
|
anthropic_api_key: "",
|
||||||
|
google_api_key: "",
|
||||||
|
grok_api_key: "",
|
||||||
|
deepseek_api_key: "",
|
||||||
|
qwen_api_key: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API settings from localStorage
|
||||||
|
*/
|
||||||
|
export function getApiSettings(): ApiSettings {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return DEFAULT_API_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
// Merge with defaults to handle any missing fields
|
||||||
|
return { ...DEFAULT_API_SETTINGS, ...parsed };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reading API settings from localStorage:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_API_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save API settings to localStorage
|
||||||
|
*/
|
||||||
|
export function saveApiSettings(settings: ApiSettings): void {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving API settings to localStorage:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear API settings from localStorage
|
||||||
|
*/
|
||||||
|
export function clearApiSettings(): void {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error clearing API settings from localStorage:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue