4f80ec1459
- Upgrade eslint and @eslint/js to v10 - Replace eslint-plugin-react-x + eslint-plugin-react-dom + eslint-plugin-react-hooks with unified @eslint-react/eslint-plugin - Raise dev Node.js requirement to ^22.13.0 || >=24 (runtime packages unaffected) - Add .npmrc with engine-strict=true - Move all @eslint-react rule overrides to eslint.config.js, eliminating plugin-specific inline eslint-disable comments - Fix real issues caught by new rules: useless assignments, leaked setTimeout, ref naming, useState setter naming
379 lines
12 KiB
TypeScript
379 lines
12 KiB
TypeScript
import {
|
|
Copy,
|
|
CornerUpLeft,
|
|
ExternalLink,
|
|
Eye,
|
|
EyeOff,
|
|
FoldVertical,
|
|
HatGlasses,
|
|
Home,
|
|
Loader2,
|
|
Scale,
|
|
UnfoldVertical,
|
|
} from 'lucide-react'
|
|
import { useEffect, useState } from 'react'
|
|
import { siGithub } from 'simple-icons'
|
|
|
|
import { DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'
|
|
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Switch } from '@/components/ui/switch'
|
|
|
|
interface ConfigPanelProps {
|
|
config: ExtConfig | null
|
|
onSave: (config: ExtConfig) => Promise<void>
|
|
onClose: () => void
|
|
}
|
|
|
|
export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
|
|
const [model, setModel] = useState(config?.model || DEMO_MODEL)
|
|
const [apiKey, setApiKey] = useState(config?.apiKey)
|
|
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
|
|
const [maxSteps, setMaxSteps] = useState(config?.maxSteps)
|
|
const [systemInstruction, setSystemInstruction] = useState(config?.systemInstruction ?? '')
|
|
const [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(
|
|
config?.experimentalLlmsTxt ?? false
|
|
)
|
|
const [experimentalIncludeAllTabs, setExperimentalIncludeAllTabs] = useState(
|
|
config?.experimentalIncludeAllTabs ?? false
|
|
)
|
|
const [disableNamedToolChoice, setDisableNamedToolChoice] = useState(
|
|
config?.disableNamedToolChoice ?? false
|
|
)
|
|
const [advancedOpen, setAdvancedOpen] = useState(false)
|
|
const [saving, setSaving] = useState(false)
|
|
const [userAuthToken, setUserAuthToken] = useState('')
|
|
const [copied, setCopied] = useState(false)
|
|
const [showToken, setShowToken] = useState(false)
|
|
const [showApiKey, setShowApiKey] = useState(false)
|
|
|
|
const [prevConfig, setPrevConfig] = useState(config)
|
|
if (prevConfig !== config) {
|
|
setPrevConfig(config)
|
|
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
|
setModel(config?.model || DEMO_MODEL)
|
|
setApiKey(config?.apiKey)
|
|
setLanguage(config?.language)
|
|
setMaxSteps(config?.maxSteps)
|
|
setSystemInstruction(config?.systemInstruction ?? '')
|
|
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
|
|
setExperimentalIncludeAllTabs(config?.experimentalIncludeAllTabs ?? false)
|
|
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
|
|
}
|
|
|
|
// Poll for user auth token every second until found
|
|
useEffect(() => {
|
|
let interval: NodeJS.Timeout | null = null
|
|
|
|
const fetchToken = async () => {
|
|
const result = await chrome.storage.local.get('PageAgentExtUserAuthToken')
|
|
const token = result.PageAgentExtUserAuthToken
|
|
if (typeof token === 'string' && token) {
|
|
setUserAuthToken(token)
|
|
if (interval) {
|
|
clearInterval(interval)
|
|
interval = null
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchToken()
|
|
interval = setInterval(fetchToken, 1000)
|
|
|
|
return () => {
|
|
if (interval) clearInterval(interval)
|
|
}
|
|
}, [])
|
|
|
|
const handleCopyToken = async () => {
|
|
if (userAuthToken) {
|
|
await navigator.clipboard.writeText(userAuthToken)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 2000)
|
|
}
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true)
|
|
try {
|
|
await onSave({
|
|
apiKey,
|
|
baseURL,
|
|
model,
|
|
language,
|
|
maxSteps: maxSteps || undefined,
|
|
systemInstruction: systemInstruction || undefined,
|
|
experimentalLlmsTxt,
|
|
experimentalIncludeAllTabs,
|
|
disableNamedToolChoice,
|
|
})
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 p-4 relative">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-base font-semibold">Settings</h2>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={onClose}
|
|
className="absolute top-2 right-3 cursor-pointer"
|
|
>
|
|
<CornerUpLeft className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* User Auth Token Section */}
|
|
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
|
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
|
|
<p className="text-[10px] text-muted-foreground mb-1">
|
|
Give a website the ability to call this extension.
|
|
</p>
|
|
<div className="flex gap-2 items-center">
|
|
<Input
|
|
readOnly
|
|
value={
|
|
userAuthToken
|
|
? showToken
|
|
? userAuthToken
|
|
: `${userAuthToken.slice(0, 4)}${'•'.repeat(userAuthToken.length - 8)}${userAuthToken.slice(-4)}`
|
|
: 'Loading...'
|
|
}
|
|
className="text-xs h-8 font-mono bg-background"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
|
onClick={() => setShowToken(!showToken)}
|
|
disabled={!userAuthToken}
|
|
>
|
|
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
|
onClick={handleCopyToken}
|
|
disabled={!userAuthToken}
|
|
>
|
|
{copied ? <span className="">✓</span> : <Copy className="size-3" />}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hub link */}
|
|
<a
|
|
href="/hub.html"
|
|
target="_blank"
|
|
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
|
>
|
|
Manage Page Agent Hub
|
|
<ExternalLink className="size-3" />
|
|
</a>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs text-muted-foreground">Base URL</label>
|
|
<Input
|
|
placeholder="https://api.openai.com/v1"
|
|
value={baseURL}
|
|
onChange={(e) => setBaseURL(e.target.value)}
|
|
className="text-xs h-8"
|
|
/>
|
|
</div>
|
|
|
|
{/* Testing API notice */}
|
|
{isTestingEndpoint(baseURL) && (
|
|
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[11px] text-muted-foreground leading-relaxed">
|
|
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
|
|
You are using our testing API. By using this you agree to the{' '}
|
|
<a
|
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="underline hover:text-foreground"
|
|
>
|
|
Terms of Use & Privacy Policy
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs text-muted-foreground">Model</label>
|
|
<Input
|
|
placeholder="gpt-5.1"
|
|
value={model}
|
|
onChange={(e) => setModel(e.target.value)}
|
|
className="text-xs h-8"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs text-muted-foreground">API Key</label>
|
|
<div className="flex gap-2 items-center">
|
|
<Input
|
|
type={showApiKey ? 'text' : 'password'}
|
|
// placeholder="sk-..."
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
className="text-xs h-8"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
|
onClick={() => setShowApiKey(!showApiKey)}
|
|
>
|
|
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs text-muted-foreground">Response Language</label>
|
|
<select
|
|
value={language ?? ''}
|
|
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
|
|
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
|
|
>
|
|
<option value="">System</option>
|
|
<option value="en-US">English</option>
|
|
<option value="zh-CN">中文</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Advanced Config */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setAdvancedOpen(!advancedOpen)}
|
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground cursor-pointer mt-1 font-bold"
|
|
>
|
|
Advanced
|
|
{advancedOpen ? <FoldVertical className="size-3" /> : <UnfoldVertical className="size-3" />}
|
|
</button>
|
|
|
|
{advancedOpen && (
|
|
<>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs text-muted-foreground">Max Steps</label>
|
|
<Input
|
|
type="number"
|
|
placeholder="40"
|
|
min={1}
|
|
max={200}
|
|
value={maxSteps ?? ''}
|
|
onChange={(e) => setMaxSteps(e.target.value ? Number(e.target.value) : undefined)}
|
|
className="text-xs h-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="text-xs text-muted-foreground">System Instruction</label>
|
|
<textarea
|
|
placeholder="Additional instructions for the agent..."
|
|
value={systemInstruction}
|
|
onChange={(e) => setSystemInstruction(e.target.value)}
|
|
rows={3}
|
|
className="text-xs rounded-md border border-input bg-background px-3 py-2 resize-y min-h-[60px]"
|
|
/>
|
|
</div>
|
|
|
|
<label className="flex items-center justify-between cursor-pointer">
|
|
<span className="text-xs text-muted-foreground">Disable named tool_choice</span>
|
|
<Switch checked={disableNamedToolChoice} onCheckedChange={setDisableNamedToolChoice} />
|
|
</label>
|
|
|
|
<label className="flex items-center justify-between cursor-pointer">
|
|
<span className="text-xs text-muted-foreground">Experimental llms.txt support</span>
|
|
<Switch checked={experimentalLlmsTxt} onCheckedChange={setExperimentalLlmsTxt} />
|
|
</label>
|
|
|
|
<label className="flex items-center justify-between cursor-pointer">
|
|
<span className="text-xs text-muted-foreground">Experimental include all tabs</span>
|
|
<Switch
|
|
checked={experimentalIncludeAllTabs}
|
|
onCheckedChange={setExperimentalIncludeAllTabs}
|
|
/>
|
|
</label>
|
|
</>
|
|
)}
|
|
|
|
<div className="flex gap-2 mt-2">
|
|
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="flex-1 h-8 text-xs cursor-pointer"
|
|
>
|
|
{saving ? <Loader2 className="size-3 animate-spin" /> : 'Save'}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground">
|
|
<div className="flex flex-col justify-between">
|
|
<span>
|
|
Version <span className="font-mono">v{__VERSION__}</span>
|
|
</span>
|
|
|
|
<a
|
|
href="https://github.com/alibaba/page-agent"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 hover:text-foreground"
|
|
>
|
|
<svg role="img" viewBox="0 0 24 24" className="size-3 fill-current">
|
|
<path d={siGithub.path} />
|
|
</svg>
|
|
<span>Source Code</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-end">
|
|
<a
|
|
href="https://alibaba.github.io/page-agent/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 hover:text-foreground"
|
|
>
|
|
<Home className="size-3" />
|
|
<span>Home Page</span>
|
|
</a>
|
|
|
|
<a
|
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 hover:text-foreground"
|
|
>
|
|
<HatGlasses className="size-3" />
|
|
<span>Privacy</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{/* attribute */}
|
|
<div className="text-[10px] text-muted-foreground bg-background fixed bottom-0 w-full flex justify-around">
|
|
<span className="leading-loose">
|
|
Built with ♥️ by{' '}
|
|
<a
|
|
href="https://github.com/gaomeng1900"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="underline hover:text-foreground"
|
|
>
|
|
@Simon
|
|
</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|