|
1 | 1 | import { Button, Input, Tooltip, Typography } from "@material-tailwind/react"
|
2 |
| -import { Fragment, useState, type ChangeEvent, type ReactNode } from "react" |
| 2 | +import { Fragment, useMemo, useState, type ChangeEvent, type ReactNode } from "react" |
3 | 3 | import { toast } from "sonner/dist"
|
4 | 4 | import type { StateProxy } from "~hooks/binding"
|
5 | 5 | import type { LLMTypes } from "~llms"
|
6 | 6 | import createLLMProvider from "~llms"
|
| 7 | +import models from "~llms/models" |
7 | 8 | import Selector from "~options/components/Selector"
|
8 | 9 |
|
9 | 10 | export type SettingSchema = {
|
@@ -57,31 +58,53 @@ function LLMSettings({ state, useHandler }: StateProxy<SettingSchema>): JSX.Elem
|
57 | 58 | const [validating, setValidating] = useState(false)
|
58 | 59 | const handler = useHandler<ChangeEvent<HTMLInputElement>, string>((e) => e.target.value)
|
59 | 60 |
|
| 61 | + const selectableModels = useMemo( |
| 62 | + () => models |
| 63 | + .filter(({ providers }) => providers.includes(state.provider)) |
| 64 | + .flatMap(({ models }) => models) |
| 65 | + .map(model => ({ label: model, value: model })), |
| 66 | + [state.provider] |
| 67 | + ) |
| 68 | + |
| 69 | + const onSwitchProvider = (provider: LLMTypes) => { |
| 70 | + state.provider = provider |
| 71 | + state.model = undefined // reset model |
| 72 | + if (provider === 'webllm') { |
| 73 | + toast.info('使用 WEBLLM 时,请确保你的电脑拥有足够的算力以供 AI 运行。') |
| 74 | + } |
| 75 | + } |
| 76 | + |
60 | 77 | const onValidate = async () => {
|
61 | 78 | setValidating(true)
|
62 |
| - try { |
63 |
| - const provider = createLLMProvider(state) |
64 |
| - await provider.validate() |
65 |
| - toast.success('配置可用!') |
66 |
| - } catch (e) { |
67 |
| - toast.error('配置不可用: ' + e.message) |
68 |
| - } finally { |
69 |
| - setValidating(false) |
70 |
| - } |
| 79 | + const provider = createLLMProvider(state) |
| 80 | + const validation = provider.validate() |
| 81 | + toast.dismiss() |
| 82 | + toast.promise(validation, { |
| 83 | + loading: '正在验证配置...', |
| 84 | + success: '配置可用!', |
| 85 | + error: err => '配置不可用: ' + (err.message ?? err), |
| 86 | + position: 'bottom-center', |
| 87 | + duration: Infinity, |
| 88 | + finally: () => setValidating(false) |
| 89 | + }) |
71 | 90 | }
|
72 | 91 |
|
| 92 | + console.log('provider: ', state.provider) |
| 93 | + console.log('model: ', state.model) |
| 94 | + |
73 | 95 | return (
|
74 | 96 | <Fragment>
|
75 | 97 | <Selector<typeof state.provider>
|
76 | 98 | className="col-span-2"
|
77 | 99 | data-testid="ai-provider"
|
78 | 100 | label="技术提供"
|
79 | 101 | value={state.provider}
|
80 |
| - onChange={e => state.provider = e} |
| 102 | + onChange={onSwitchProvider} |
81 | 103 | options={[
|
82 |
| - { label: 'Cloudflare AI', value: 'cloudflare' }, |
83 |
| - { label: '公共服务器', value: 'worker' }, |
84 |
| - { label: 'Chrome 浏览器内置 AI', value: 'nano' } |
| 104 | + { label: 'Cloudflare AI (云)', value: 'cloudflare' }, |
| 105 | + { label: '公共服务器 (云)', value: 'worker' }, |
| 106 | + { label: 'Chrome 浏览器内置 AI (本地)', value: 'nano' }, |
| 107 | + { label: 'Web LLM (本地)', value: 'webllm' } |
85 | 108 | ]}
|
86 | 109 | />
|
87 | 110 | {state.provider === 'cloudflare' && (
|
@@ -110,27 +133,22 @@ function LLMSettings({ state, useHandler }: StateProxy<SettingSchema>): JSX.Elem
|
110 | 133 | />
|
111 | 134 | </Fragment>
|
112 | 135 | )}
|
113 |
| - {['cloudflare', 'worker'].includes(state.provider) && ( |
| 136 | + {state.provider === 'nano' && ( |
| 137 | + <Hints> |
| 138 | + <Typography className="underline" as="a" href="https://juejin.cn/post/7401036139384143910" target="_blank">点击此处</Typography> |
| 139 | + 查看如何启用 Chrome 浏览器内置 AI |
| 140 | + </Hints> |
| 141 | + )} |
| 142 | + {selectableModels.length > 0 && ( |
114 | 143 | <Selector<string>
|
115 | 144 | data-testid="ai-model"
|
116 | 145 | label="模型提供"
|
117 | 146 | value={state.model}
|
118 | 147 | onChange={e => state.model = e}
|
119 |
| - options={[ |
120 |
| - { label: '@cf/qwen/qwen1.5-14b-chat-awq', value: '@cf/qwen/qwen1.5-14b-chat-awq' }, |
121 |
| - { label: '@cf/qwen/qwen1.5-7b-chat-awq', value: '@cf/qwen/qwen1.5-7b-chat-awq' }, |
122 |
| - { label: '@cf/qwen/qwen1.5-1.8b-chat', value: '@cf/qwen/qwen1.5-1.8b-chat' }, |
123 |
| - { label: '@hf/google/gemma-7b-it', value: '@hf/google/gemma-7b-it' }, |
124 |
| - { label: '@hf/nousresearch/hermes-2-pro-mistral-7b', value: '@hf/nousresearch/hermes-2-pro-mistral-7b' } |
125 |
| - ]} |
| 148 | + options={selectableModels} |
| 149 | + emptyValue="默认" |
126 | 150 | />
|
127 | 151 | )}
|
128 |
| - {state.provider === 'nano' && ( |
129 |
| - <Hints> |
130 |
| - <Typography className="underline" as="a" href="https://juejin.cn/post/7401036139384143910" target="_blank">点击此处</Typography> |
131 |
| - 查看如何启用 Chrome 浏览器内置 AI |
132 |
| - </Hints> |
133 |
| - )} |
134 | 152 | <div className="col-span-2">
|
135 | 153 | <Button disabled={validating} onClick={onValidate} color="blue" size="lg" className="group flex items-center justify-center gap-3 text-[1rem] hover:shadow-lg">
|
136 | 154 | 验证是否可用
|
|
0 commit comments