mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
feat: add Accordion component for UI and Zustand store for coordinator management
- Implemented Accordion component using Radix UI for collapsible sections. - Created Zustand store to manage coordinator execution state, command chains, logs, and interactive questions. - Added validation tests for CLI settings type definitions, ensuring type safety and correct behavior of helper functions.
This commit is contained in:
@@ -119,7 +119,7 @@ function CliSettingsCard({
|
||||
)}
|
||||
{cliSettings.settings.includeCoAuthoredBy !== undefined && (
|
||||
<span>
|
||||
Co-authored: {cliSettings.settings.includeCoAuthoredBy ? 'Yes' : 'No'}
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.coAuthoredBy' })}: {formatMessage({ id: cliSettings.settings.includeCoAuthoredBy ? 'common.yes' : 'common.no' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Check, Eye, EyeOff } from 'lucide-react';
|
||||
import { Check, Eye, EyeOff, X, Plus } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -59,12 +59,21 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
||||
const [providerId, setProviderId] = useState('');
|
||||
const [model, setModel] = useState('sonnet');
|
||||
const [includeCoAuthoredBy, setIncludeCoAuthoredBy] = useState(false);
|
||||
const [settingsFile, setSettingsFile] = useState('');
|
||||
|
||||
// Direct mode state
|
||||
const [authToken, setAuthToken] = useState('');
|
||||
const [baseUrl, setBaseUrl] = useState('');
|
||||
const [showToken, setShowToken] = useState(false);
|
||||
|
||||
// Available models state
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [modelInput, setModelInput] = useState('');
|
||||
|
||||
// Tags state
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
|
||||
// Validation errors
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -76,6 +85,9 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
||||
setEnabled(cliSettings.enabled);
|
||||
setModel(cliSettings.settings.model || 'sonnet');
|
||||
setIncludeCoAuthoredBy(cliSettings.settings.includeCoAuthoredBy || false);
|
||||
setSettingsFile(cliSettings.settings.settingsFile || '');
|
||||
setAvailableModels(cliSettings.settings.availableModels || []);
|
||||
setTags(cliSettings.settings.tags || []);
|
||||
|
||||
// Determine mode based on settings
|
||||
const hasCustomBaseUrl = Boolean(
|
||||
@@ -104,8 +116,13 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
||||
setProviderId('');
|
||||
setModel('sonnet');
|
||||
setIncludeCoAuthoredBy(false);
|
||||
setSettingsFile('');
|
||||
setAuthToken('');
|
||||
setBaseUrl('');
|
||||
setAvailableModels([]);
|
||||
setModelInput('');
|
||||
setTags([]);
|
||||
setTagInput('');
|
||||
setErrors({});
|
||||
}
|
||||
}, [cliSettings, open, providers]);
|
||||
@@ -183,6 +200,9 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
||||
env,
|
||||
model,
|
||||
includeCoAuthoredBy,
|
||||
settingsFile: settingsFile.trim() || undefined,
|
||||
availableModels,
|
||||
tags,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -198,6 +218,37 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
||||
}
|
||||
};
|
||||
|
||||
// Handle add model
|
||||
const handleAddModel = () => {
|
||||
const newModel = modelInput.trim();
|
||||
if (newModel && !availableModels.includes(newModel)) {
|
||||
setAvailableModels([...availableModels, newModel]);
|
||||
setModelInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle remove model
|
||||
const handleRemoveModel = (modelToRemove: string) => {
|
||||
setAvailableModels(availableModels.filter((m) => m !== modelToRemove));
|
||||
};
|
||||
|
||||
// Handle add tag
|
||||
const handleAddTag = () => {
|
||||
const newTag = tagInput.trim();
|
||||
if (newTag && !tags.includes(newTag)) {
|
||||
setTags([...tags, newTag]);
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle remove tag
|
||||
const handleRemoveTag = (tagToRemove: string) => {
|
||||
setTags(tags.filter((t) => t !== tagToRemove));
|
||||
};
|
||||
|
||||
// Predefined tags
|
||||
const predefinedTags = ['分析', 'Debug', 'implementation', 'refactoring', 'testing'];
|
||||
|
||||
// Get selected provider info
|
||||
const selectedProvider = providers.find((p) => p.id === providerId);
|
||||
|
||||
@@ -387,15 +438,154 @@ export function CliSettingsModal({ open, onClose, cliSettings }: CliSettingsModa
|
||||
</Tabs>
|
||||
|
||||
{/* Additional Settings (both modes) */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="coAuthored"
|
||||
checked={includeCoAuthoredBy}
|
||||
onCheckedChange={setIncludeCoAuthoredBy}
|
||||
/>
|
||||
<Label htmlFor="coAuthored" className="cursor-pointer">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.includeCoAuthoredBy' })}
|
||||
</Label>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="coAuthored"
|
||||
checked={includeCoAuthoredBy}
|
||||
onCheckedChange={setIncludeCoAuthoredBy}
|
||||
/>
|
||||
<Label htmlFor="coAuthored" className="cursor-pointer">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.includeCoAuthoredBy' })}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="settingsFile">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.settingsFile' })}
|
||||
</Label>
|
||||
<Input
|
||||
id="settingsFile"
|
||||
value={settingsFile}
|
||||
onChange={(e) => setSettingsFile(e.target.value)}
|
||||
placeholder={formatMessage({ id: 'apiSettings.cliSettings.settingsFilePlaceholder' })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Available Models Section */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="availableModels">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.availableModels' })}
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded-lg min-h-[60px]">
|
||||
{availableModels.map((model) => (
|
||||
<span
|
||||
key={model}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm"
|
||||
>
|
||||
{model}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveModel(model)}
|
||||
className="hover:text-destructive transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<div className="flex gap-2 flex-1">
|
||||
<Input
|
||||
id="availableModels"
|
||||
value={modelInput}
|
||||
onChange={(e) => setModelInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddModel();
|
||||
}
|
||||
}}
|
||||
placeholder={formatMessage({ id: 'apiSettings.cliSettings.availableModelsPlaceholder' })}
|
||||
className="flex-1 min-w-[120px]"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleAddModel}
|
||||
variant="outline"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.availableModelsHint' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tags Section */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tags">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.tags' })}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.tagsDescription' })}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded-lg min-h-[60px]">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="hover:text-destructive transition-colors"
|
||||
aria-label={formatMessage({ id: 'apiSettings.cliSettings.removeTag' })}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<div className="flex gap-2 flex-1">
|
||||
<Input
|
||||
id="tags"
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddTag();
|
||||
}
|
||||
}}
|
||||
placeholder={formatMessage({ id: 'apiSettings.cliSettings.tagInputPlaceholder' })}
|
||||
className="flex-1 min-w-[120px]"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleAddTag}
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Predefined Tags */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.predefinedTags' })}:
|
||||
</span>
|
||||
{predefinedTags.map((predefinedTag) => (
|
||||
<button
|
||||
key={predefinedTag}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!tags.includes(predefinedTag)) {
|
||||
setTags([...tags, predefinedTag]);
|
||||
}
|
||||
}}
|
||||
disabled={tags.includes(predefinedTag)}
|
||||
className="text-xs px-2 py-0.5 rounded border border-border hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{predefinedTag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user