This commit is contained in:
MarkLo 2025-12-13 07:14:45 +08:00
parent 8b2dbe9437
commit c9f6e6a8d1
2 changed files with 175 additions and 84 deletions

View File

@ -121,6 +121,41 @@
} }
} }
/* Safari mobile touch fixes */
@layer base {
/* Fix for Safari iOS touch events on buttons and interactive elements */
button,
[role="button"],
input[type="submit"],
input[type="button"],
.cursor-pointer {
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
-webkit-touch-callout: none;
}
/* Ensure form elements are properly interactive on iOS */
select,
input,
textarea {
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
}
/* Fix for Radix UI popover/select on iOS Safari */
[data-radix-popper-content-wrapper] {
touch-action: auto !important;
}
/* Ensure buttons inside forms work on iOS */
form button[type="submit"] {
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
}
/* Custom animations */ /* Custom animations */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
@ -186,7 +221,8 @@
/* Heartbeat animation for buttons */ /* Heartbeat animation for buttons */
@keyframes heartbeat { @keyframes heartbeat {
0%, 100% { 0%,
100% {
transform: scale(1); transform: scale(1);
} }
10% { 10% {
@ -283,19 +319,19 @@
/* Gradient Backgrounds - Blue/Pink in light, Blue/Purple in dark */ /* Gradient Backgrounds - Blue/Pink in light, Blue/Purple in dark */
.gradient-bg-primary { .gradient-bg-primary {
background: linear-gradient(135deg, #3B82F6 0%, #EC4899 100%); background: linear-gradient(135deg, #3b82f6 0%, #ec4899 100%);
} }
.dark .gradient-bg-primary { .dark .gradient-bg-primary {
background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
} }
.gradient-bg-secondary { .gradient-bg-secondary {
background: linear-gradient(135deg, #60A5FA 0%, #F472B6 100%); background: linear-gradient(135deg, #60a5fa 0%, #f472b6 100%);
} }
.dark .gradient-bg-secondary { .dark .gradient-bg-secondary {
background: linear-gradient(135deg, #60A5FA 0%, #A78BFA 100%); background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
} }
.gradient-bg-accent { .gradient-bg-accent {
@ -436,28 +472,28 @@
/* Gradient Text - Blue/Pink in light, Blue/Purple in dark */ /* Gradient Text - Blue/Pink in light, Blue/Purple in dark */
.gradient-text-primary { .gradient-text-primary {
background: linear-gradient(135deg, #3B82F6 0%, #EC4899 100%); background: linear-gradient(135deg, #3b82f6 0%, #ec4899 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.dark .gradient-text-primary { .dark .gradient-text-primary {
background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.gradient-text-secondary { .gradient-text-secondary {
background: linear-gradient(135deg, #60A5FA 0%, #F472B6 100%); background: linear-gradient(135deg, #60a5fa 0%, #f472b6 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.dark .gradient-text-secondary { .dark .gradient-text-secondary {
background: linear-gradient(135deg, #60A5FA 0%, #A78BFA 100%); background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@ -635,5 +671,3 @@
box-shadow: 0 4px 20px 0 rgba(147, 51, 234, 0.2), box-shadow: 0 4px 20px 0 rgba(147, 51, 234, 0.2),
inset 0 1px 0 0 rgba(255, 255, 255, 0.1); inset 0 1px 0 0 rgba(255, 255, 255, 0.1);
} }

View File

@ -79,8 +79,8 @@ const formSchema = z.object({
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
embedding_api_key: z.string().min(1, "請輸入嵌入模型 API Key"), embedding_api_key: z.string().min(1, "請輸入嵌入模型 API Key"),
alpha_vantage_api_key: z.string().optional().or(z.literal("")), // 選填 alpha_vantage_api_key: z.string().optional().or(z.literal("")), // 選填
finmind_api_key: z.string().optional().or(z.literal("")), // 選填 finmind_api_key: z.string().optional().or(z.literal("")), // 選填
}); });
interface AnalysisFormProps { interface AnalysisFormProps {
@ -133,24 +133,54 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
// For custom models, always use custom base URL and API key // For custom models, always use custom base URL and API key
if (isQuickThinkCustom) { if (isQuickThinkCustom) {
form.setValue("quick_think_base_url", savedSettings.custom_base_url || ""); form.setValue(
form.setValue("quick_think_api_key", savedSettings.custom_api_key || ""); "quick_think_base_url",
savedSettings.custom_base_url || ""
);
form.setValue(
"quick_think_api_key",
savedSettings.custom_api_key || ""
);
} else { } else {
form.setValue("quick_think_base_url", getBaseUrlForModel(quickThinkLlm, savedSettings.custom_base_url)); form.setValue(
form.setValue("quick_think_api_key", getApiKeyForModel(quickThinkLlm, savedSettings)); "quick_think_base_url",
getBaseUrlForModel(quickThinkLlm, savedSettings.custom_base_url)
);
form.setValue(
"quick_think_api_key",
getApiKeyForModel(quickThinkLlm, savedSettings)
);
} }
if (isDeepThinkCustom) { if (isDeepThinkCustom) {
form.setValue("deep_think_base_url", savedSettings.custom_base_url || ""); form.setValue(
"deep_think_base_url",
savedSettings.custom_base_url || ""
);
form.setValue("deep_think_api_key", savedSettings.custom_api_key || ""); form.setValue("deep_think_api_key", savedSettings.custom_api_key || "");
} else { } else {
form.setValue("deep_think_base_url", getBaseUrlForModel(deepThinkLlm, savedSettings.custom_base_url)); form.setValue(
form.setValue("deep_think_api_key", getApiKeyForModel(deepThinkLlm, savedSettings)); "deep_think_base_url",
getBaseUrlForModel(deepThinkLlm, savedSettings.custom_base_url)
);
form.setValue(
"deep_think_api_key",
getApiKeyForModel(deepThinkLlm, savedSettings)
);
} }
form.setValue("embedding_base_url", savedSettings.custom_base_url || "https://api.openai.com/v1"); form.setValue(
form.setValue("embedding_api_key", savedSettings.custom_api_key || savedSettings.openai_api_key); "embedding_base_url",
form.setValue("alpha_vantage_api_key", savedSettings.alpha_vantage_api_key || ""); savedSettings.custom_base_url || "https://api.openai.com/v1"
);
form.setValue(
"embedding_api_key",
savedSettings.custom_api_key || savedSettings.openai_api_key
);
form.setValue(
"alpha_vantage_api_key",
savedSettings.alpha_vantage_api_key || ""
);
form.setValue("finmind_api_key", savedSettings.finmind_api_key || ""); form.setValue("finmind_api_key", savedSettings.finmind_api_key || "");
}; };
@ -163,8 +193,12 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
const currentTicker = form.getValues("ticker"); const currentTicker = form.getValues("ticker");
// 只在用戶未修改預設值時才自動切換 // 只在用戶未修改預設值時才自動切換
const isTwStock = marketType === "twse" || marketType === "tpex"; const isTwStock = marketType === "twse" || marketType === "tpex";
const isDefaultUsTicker = currentTicker === "NVDA" || currentTicker === "AAPL"; const isDefaultUsTicker =
const isDefaultTwTicker = currentTicker === "2330" || currentTicker === "2317" || currentTicker === "6488"; currentTicker === "NVDA" || currentTicker === "AAPL";
const isDefaultTwTicker =
currentTicker === "2330" ||
currentTicker === "2317" ||
currentTicker === "6488";
if (isTwStock && isDefaultUsTicker) { if (isTwStock && isDefaultUsTicker) {
form.setValue("ticker", marketType === "twse" ? "2330" : "6488"); form.setValue("ticker", marketType === "twse" ? "2330" : "6488");
@ -189,19 +223,24 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
function handleSubmit(values: z.infer<typeof formSchema>) { function handleSubmit(values: z.infer<typeof formSchema>) {
// Use custom model names if "custom" is selected // Use custom model names if "custom" is selected
const finalQuickThinkLlm = values.quick_think_llm === "custom" const finalQuickThinkLlm =
? values.custom_quick_think_model || "" values.quick_think_llm === "custom"
: values.quick_think_llm; ? values.custom_quick_think_model || ""
: values.quick_think_llm;
const finalDeepThinkLlm = values.deep_think_llm === "custom" const finalDeepThinkLlm =
? values.custom_deep_think_model || "" values.deep_think_llm === "custom"
: values.deep_think_llm; ? values.custom_deep_think_model || ""
: values.deep_think_llm;
// Validate custom model names // Validate custom model names
if (values.quick_think_llm === "custom" && !values.custom_quick_think_model) { if (
values.quick_think_llm === "custom" &&
!values.custom_quick_think_model
) {
form.setError("custom_quick_think_model", { form.setError("custom_quick_think_model", {
type: "manual", type: "manual",
message: "請輸入快速思維模型的完整名稱" message: "請輸入快速思維模型的完整名稱",
}); });
return; return;
} }
@ -209,7 +248,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
if (values.deep_think_llm === "custom" && !values.custom_deep_think_model) { if (values.deep_think_llm === "custom" && !values.custom_deep_think_model) {
form.setError("custom_deep_think_model", { form.setError("custom_deep_think_model", {
type: "manual", type: "manual",
message: "請輸入深層思維模型的完整名稱" message: "請輸入深層思維模型的完整名稱",
}); });
return; return;
} }
@ -255,17 +294,18 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormItem> <FormItem>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{ANALYSTS.map((analyst) => { {ANALYSTS.map((analyst) => {
const isSelected = field.value?.includes(analyst.value); const isSelected = field.value?.includes(
analyst.value
);
return ( return (
<FormItem <FormItem key={analyst.value} className="space-y-0">
key={analyst.value}
className="space-y-0"
>
<FormControl> <FormControl>
<div <div
onClick={() => { onClick={() => {
const newValue = isSelected const newValue = isSelected
? field.value?.filter((v: string) => v !== analyst.value) ? field.value?.filter(
(v: string) => v !== analyst.value
)
: [...(field.value ?? []), analyst.value]; : [...(field.value ?? []), analyst.value];
field.onChange(newValue); field.onChange(newValue);
}} }}
@ -284,9 +324,13 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
: "border-muted-foreground" : "border-muted-foreground"
)} )}
> >
{isSelected && <CheckIcon className="h-3.5 w-3.5" />} {isSelected && (
<CheckIcon className="h-3.5 w-3.5" />
)}
</div> </div>
<span className="font-medium select-none">{analyst.label}</span> <span className="font-medium select-none">
{analyst.label}
</span>
</div> </div>
</FormControl> </FormControl>
</FormItem> </FormItem>
@ -318,20 +362,27 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="us" className="py-3 cursor-pointer"> <SelectItem
value="us"
className="py-3 cursor-pointer"
>
🇺🇸 🇺🇸
</SelectItem> </SelectItem>
<SelectItem value="twse" className="py-3 cursor-pointer"> <SelectItem
value="twse"
className="py-3 cursor-pointer"
>
🇹🇼 🇹🇼
</SelectItem> </SelectItem>
<SelectItem value="tpex" className="py-3 cursor-pointer"> <SelectItem
value="tpex"
className="py-3 cursor-pointer"
>
🇹🇼 / 🇹🇼 /
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription> <FormDescription></FormDescription>
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -347,8 +398,11 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormControl> <FormControl>
<Input <Input
placeholder={ placeholder={
marketType === "us" ? "NVDA" : marketType === "us"
marketType === "twse" ? "2330" : "6488" ? "NVDA"
: marketType === "twse"
? "2330"
: "6488"
} }
{...field} {...field}
/> />
@ -358,8 +412,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
? "輸入美股代碼例如NVDA、AAPL" ? "輸入美股代碼例如NVDA、AAPL"
: marketType === "twse" : marketType === "twse"
? "輸入上市股票代碼例如2330、2317" ? "輸入上市股票代碼例如2330、2317"
: "輸入上櫃/興櫃股票代碼例如6488、5765" : "輸入上櫃/興櫃股票代碼例如6488、5765"}
}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -376,7 +429,9 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<DatePicker <DatePicker
date={field.value ? new Date(field.value) : undefined} date={field.value ? new Date(field.value) : undefined}
onDateChange={(date) => { onDateChange={(date) => {
field.onChange(date ? format(date, "yyyy-MM-dd") : "") field.onChange(
date ? format(date, "yyyy-MM-dd") : ""
);
}} }}
placeholder="選擇分析日期" placeholder="選擇分析日期"
className="w-full" className="w-full"
@ -557,10 +612,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormItem className="md:col-span-3 animate-scale-up"> <FormItem className="md:col-span-3 animate-scale-up">
<FormLabel></FormLabel> <FormLabel></FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="例如deepseek-chat" {...field} />
placeholder="例如deepseek-chat"
{...field}
/>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
使 使
@ -702,10 +754,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
<FormItem className="md:col-span-3 animate-scale-up"> <FormItem className="md:col-span-3 animate-scale-up">
<FormLabel></FormLabel> <FormLabel></FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="例如deepseek-chat" {...field} />
placeholder="例如deepseek-chat"
{...field}
/>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
使 使
@ -723,6 +772,14 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
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"
disabled={loading} disabled={loading}
size="lg" size="lg"
style={{
touchAction: "manipulation",
WebkitTapHighlightColor: "transparent",
}}
onClick={(e) => {
// Ensure touch events work on Safari mobile
e.currentTarget.blur();
}}
> >
{loading ? "執行分析中..." : "執行分析"} {loading ? "執行分析中..." : "執行分析"}
</Button> </Button>