Skip to content

Commit 9c81a72

Browse files
committed
finished ai settings page
1 parent c912040 commit 9c81a72

File tree

4 files changed

+147
-3
lines changed

4 files changed

+147
-3
lines changed

src/options/components/Hints.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ function Hints(props: HintsProps): JSX.Element {
1616
<Fragment>
1717
<Typography
1818
variant="small"
19-
2019
className="mt-2 flex items-center gap-1 font-normal dark:text-gray-400"
2120
>
2221
<svg

src/options/components/Selector.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type SelectorProps<T> = {
1414
label: string
1515
disabled?: boolean
1616
options: SelectorOption<T>[]
17+
className?: string
1718
}
1819

1920

@@ -32,7 +33,7 @@ function Selector<T = any>(props: SelectorProps<T>): JSX.Element {
3233
}
3334

3435
return (
35-
<section className={`relative block text-left`} data-testid={props['data-testid']}>
36+
<section className={`relative block text-left ${props.className || ''}`} data-testid={props['data-testid']}>
3637
<label className="text-sm ml-1 font-medium text-gray-900 dark:text-white">{props.label}</label>
3738
<div ref={dropdownRef} className={`mt-2 ${props.disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`} onClick={() => !props.disabled && setOpen(!isOpen)}>
3839
<div className={`inline-flex justify-between h-full w-full rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 text-sm font-medium text-gray-700 dark:text-white ${props.disabled ? 'opacity-50 bg-transparent' : 'bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900'} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 dark:focus:ring-gray-500`}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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

src/options/features/jimaku/index.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ButtonFragment, { buttonDefaultSettings, type ButtonSchema } from "./comp
1010
import DanmakuZone, { danmakuDefaultSettings, type DanmakuSchema } from "./components/DanmakuFragment"
1111
import JimakuZone, { jimakuDefaultSettings, type JimakuSchema } from "./components/JimakuFragment"
1212
import ListingFragment, { listingDefaultSettings, type ListingSchema } from "./components/ListingFragment"
13+
import AIFragment, { aiDefaultSettings, type AISchema } from "./components/AIFragment"
1314

1415

1516
export const title: string = '同传弹幕过滤'
@@ -28,6 +29,7 @@ export type FeatureSettingSchema = {
2829
danmakuZone: DanmakuSchema,
2930
buttonZone: ButtonSchema,
3031
listingZone: ListingSchema
32+
aiZone: AISchema
3133
}
3234

3335
export const defaultSettings: Readonly<FeatureSettingSchema> = {
@@ -39,7 +41,8 @@ export const defaultSettings: Readonly<FeatureSettingSchema> = {
3941
jimakuZone: jimakuDefaultSettings,
4042
danmakuZone: danmakuDefaultSettings,
4143
buttonZone: buttonDefaultSettings,
42-
listingZone: listingDefaultSettings
44+
listingZone: listingDefaultSettings,
45+
aiZone: aiDefaultSettings
4346
}
4447

4548
const zones: {
@@ -66,6 +69,11 @@ const zones: {
6669
Zone: ListingFragment,
6770
title: '同传名单设定',
6871
key: 'listingZone'
72+
},
73+
{
74+
Zone: AIFragment,
75+
title: 'AI 设定',
76+
key: 'aiZone'
6977
}
7078
]
7179

0 commit comments

Comments
 (0)