1
+ import { Button , Input , List , Tooltip , Typography } from "@material-tailwind/react"
2
+ import { type ChangeEvent , Fragment , useState } from "react"
3
+ import { toast } from "sonner/dist"
4
+ import type { StateProxy } from "~hooks/binding"
5
+ import type { LLMProviders , LLMTypes } from "~llms"
6
+ import createLLMProvider from "~llms"
7
+ import ExperienmentFeatureIcon from "~options/components/ExperientmentFeatureIcon"
8
+ import Selector from "~options/components/Selector"
9
+ import SwitchListItem from "~options/components/SwitchListItem"
10
+
11
+
12
+
13
+ export type AISchema = {
14
+ enabled : boolean
15
+ provider : LLMTypes
16
+
17
+ // cloudflare settings
18
+ accountId ?: string
19
+ apiToken ?: string
20
+ }
21
+
22
+
23
+ export const aiDefaultSettings : Readonly < AISchema > = {
24
+ enabled : false ,
25
+ provider : 'worker'
26
+ }
27
+
28
+
29
+ function AIFragment ( { state, useHandler } : StateProxy < AISchema > ) : JSX . Element {
30
+
31
+ const [ validating , setValidating ] = useState ( false )
32
+
33
+ const handler = useHandler < ChangeEvent < HTMLInputElement > , string > ( ( e ) => e . target . value )
34
+ const checker = useHandler < ChangeEvent < HTMLInputElement > , boolean > ( ( e ) => e . target . checked )
35
+
36
+ const onValidate = async ( ) => {
37
+ setValidating ( true )
38
+ try {
39
+ let provider : LLMProviders ;
40
+ if ( state . provider === 'qwen' ) {
41
+ provider = await createLLMProvider ( state . provider , state . accountId , state . apiToken )
42
+ } else {
43
+ provider = await createLLMProvider ( state . provider )
44
+ }
45
+ await provider . validate ( )
46
+ toast . success ( '配置可用!' )
47
+ } catch ( e ) {
48
+ toast . error ( '配置不可用: ' + e . message )
49
+ } finally {
50
+ setValidating ( false )
51
+ }
52
+ }
53
+
54
+ return (
55
+ < Fragment >
56
+ < List className = "col-span-2 border border-[#808080] rounded-md" >
57
+ < SwitchListItem
58
+ data-testid = "ai-enabled"
59
+ label = "启用同传字幕AI总结"
60
+ hint = "此功能将采用通义大模型对同传字幕进行总结"
61
+ value = { state . enabled }
62
+ onChange = { checker ( 'enabled' ) }
63
+ marker = { < ExperienmentFeatureIcon /> }
64
+ />
65
+ </ List >
66
+ { state . enabled && (
67
+ < Fragment >
68
+ < Selector < typeof state . provider >
69
+ className = "col-span-2"
70
+ data-testid = "ai-provider"
71
+ label = "AI 提供商"
72
+ value = { state . provider }
73
+ onChange = { e => state . provider = e }
74
+ options = { [
75
+ { label : 'Cloudflare AI' , value : 'qwen' } ,
76
+ { label : '有限度服务器' , value : 'worker' } ,
77
+ { label : 'Chrome 浏览器内置 AI' , value : 'nano' }
78
+ ] }
79
+ />
80
+ { state . provider === 'qwen' && (
81
+ < Fragment >
82
+ < Typography
83
+ className = "flex items-center gap-1 font-normal dark:text-gray-200 col-span-2"
84
+ >
85
+ < svg
86
+ xmlns = "http://www.w3.org/2000/svg"
87
+ viewBox = "0 0 24 24"
88
+ fill = "currentColor"
89
+ className = "-mt-px h-6 w-6"
90
+ >
91
+ < path
92
+ fillRule = "evenodd"
93
+ d = "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z"
94
+ clipRule = "evenodd"
95
+ />
96
+ </ svg >
97
+ < Typography className = "underline" as = "a" href = "https://linux.do/t/topic/34037" target = "_blank" > 点击此处</ Typography >
98
+ 查看如何获得 Cloudflare API Token 和 Account ID
99
+ </ Typography >
100
+ < Input
101
+ data-testid = "cf-account-id"
102
+ crossOrigin = "anonymous"
103
+ variant = "static"
104
+ required
105
+ label = "Cloudflare Account ID"
106
+ value = { state . accountId }
107
+ onChange = { handler ( 'accountId' ) }
108
+ />
109
+ < Input
110
+ data-testid = "cf-api-token"
111
+ crossOrigin = "anonymous"
112
+ variant = "static"
113
+ required
114
+ label = "Cloudflare API Token"
115
+ value = { state . apiToken }
116
+ onChange = { handler ( 'apiToken' ) }
117
+ />
118
+ </ Fragment >
119
+ ) }
120
+ </ Fragment >
121
+ ) }
122
+ < div className = "col-span-2" >
123
+ < Button disabled = { validating } onClick = { onValidate } color = "blue" size = "lg" className = "group flex items-center justify-center gap-3 text-[1rem] hover:shadow-lg" >
124
+ 验证是否可用
125
+ < Tooltip content = "检查你目前的配置是否可用。若不可用,则无法启用AI总结功能。" placement = "top-end" >
126
+ < svg xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" strokeWidth = { 1.5 } stroke = "currentColor" className = "size-6" >
127
+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
128
+ </ svg >
129
+ </ Tooltip >
130
+ </ Button >
131
+ </ div >
132
+ </ Fragment >
133
+ )
134
+ }
135
+
136
+ export default AIFragment
0 commit comments