mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
feat: add templates for epics, product brief, and requirements PRD
- Created a new directory structure for epics and stories with templates for individual epics and an index file. - Added a product brief template for generating product brief documents in Phase 2. - Introduced a requirements PRD template for generating a Product Requirements Document as a directory of individual requirement files in Phase 3. feat: implement V2PipelineTab component for Memory V2 management - Developed the V2PipelineTab component to manage extraction and consolidation processes. - Included ExtractionCard and ConsolidationCard components to handle respective functionalities. - Added JobsList component to display job statuses and allow filtering by job kind. feat: create hooks for Memory V2 pipeline - Implemented custom hooks for managing extraction and consolidation statuses, as well as job listings. - Added mutation hooks to trigger extraction and consolidation processes with automatic query invalidation on success.
This commit is contained in:
@@ -187,10 +187,7 @@ export function GlobalSettingsTab() {
|
||||
});
|
||||
|
||||
// Fetch project-tech stats
|
||||
const {
|
||||
data: projectTechStats,
|
||||
isLoading: isLoadingProjectTech,
|
||||
} = useQuery({
|
||||
const { data: projectTechStats } = useQuery({
|
||||
queryKey: settingsKeys.projectTech(),
|
||||
queryFn: fetchProjectTechStats,
|
||||
staleTime: 60000, // 1 minute
|
||||
@@ -490,7 +487,7 @@ export function GlobalSettingsTab() {
|
||||
)}
|
||||
onClick={() => localDevProgress.enabled && handleCategoryToggle(cat)}
|
||||
>
|
||||
{cat} ({projectTechStats?.categories[cat] || 0})
|
||||
{formatMessage({ id: `specs.devCategory.${cat}`, defaultMessage: cat })} ({projectTechStats?.categories[cat] || 0})
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
||||
import type { InjectionPreviewFile, InjectionPreviewResponse } from '@/lib/api';
|
||||
import { getInjectionPreview } from '@/lib/api';
|
||||
import { getInjectionPreview, COMMAND_PREVIEWS, type CommandPreviewConfig } from '@/lib/api';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
@@ -209,6 +209,12 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||
const [previewFile, setPreviewFile] = useState<InjectionPreviewFile | null>(null);
|
||||
|
||||
// State for command preview
|
||||
const [selectedCommand, setSelectedCommand] = useState<CommandPreviewConfig>(COMMAND_PREVIEWS[0]);
|
||||
const [commandPreviewData, setCommandPreviewData] = useState<InjectionPreviewResponse | null>(null);
|
||||
const [commandPreviewLoading, setCommandPreviewLoading] = useState(false);
|
||||
const [commandPreviewDialogOpen, setCommandPreviewDialogOpen] = useState(false);
|
||||
|
||||
// Fetch stats
|
||||
const loadStats = useCallback(async () => {
|
||||
setStatsLoading(true);
|
||||
@@ -264,6 +270,21 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
}
|
||||
}, [previewMode]);
|
||||
|
||||
// Load command preview content
|
||||
const loadCommandPreview = useCallback(async (command: CommandPreviewConfig) => {
|
||||
setCommandPreviewLoading(true);
|
||||
try {
|
||||
const data = await getInjectionPreview(command.mode, true, undefined, command.category);
|
||||
setCommandPreviewData(data);
|
||||
setSelectedCommand(command);
|
||||
setCommandPreviewDialogOpen(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to load command preview:', err);
|
||||
} finally {
|
||||
setCommandPreviewLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
@@ -406,15 +427,20 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
return stats;
|
||||
}, [previewData]);
|
||||
|
||||
// Calculate progress and status
|
||||
// Calculate progress and status - use API's maxLength for consistency
|
||||
const currentLength = stats?.injectionLength?.withKeywords || 0;
|
||||
const maxLength = settings.maxLength;
|
||||
const apiMaxLength = stats?.injectionLength?.maxLength || settings.maxLength;
|
||||
const maxLength = apiMaxLength; // Use API's maxLength for consistency
|
||||
const warnThreshold = settings.warnThreshold;
|
||||
const percentage = calculatePercentage(currentLength, maxLength);
|
||||
const isOverLimit = currentLength > maxLength;
|
||||
const isOverWarning = currentLength > warnThreshold;
|
||||
const remainingSpace = Math.max(0, maxLength - currentLength);
|
||||
|
||||
// Calculate approximate line count (assuming ~80 chars per line)
|
||||
const estimatedLineCount = Math.ceil(currentLength / 80);
|
||||
const maxLineCount = Math.ceil(maxLength / 80);
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-6', className)}>
|
||||
{/* Recommended Hooks Section */}
|
||||
@@ -556,36 +582,40 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
>
|
||||
{formatNumber(currentLength)} / {formatNumber(maxLength)}{' '}
|
||||
{formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
|
||||
<span className="text-muted-foreground ml-2">
|
||||
(~{formatNumber(estimatedLineCount)} / {formatNumber(maxLineCount)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={percentage}
|
||||
className={cn(
|
||||
'h-3',
|
||||
isOverLimit && 'bg-destructive/20',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-100 dark:bg-yellow-900/30'
|
||||
)}
|
||||
indicatorClassName={cn(
|
||||
isOverLimit && 'bg-destructive',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-500'
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Warning threshold marker */}
|
||||
<div
|
||||
className="relative h-0"
|
||||
style={{
|
||||
left: `${Math.min(100, (warnThreshold / maxLength) * 100)}%`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute -top-5 transform -translate-x-1/2 flex flex-col items-center">
|
||||
<AlertTriangle className="h-3 w-3 text-yellow-500" />
|
||||
<div className="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{formatMessage({ id: 'specs.injection.warnThreshold', defaultMessage: 'Warn' })}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
|
||||
<span>{percentage}%</span>
|
||||
<span>{formatMessage({ id: 'specs.injection.maxLimit', defaultMessage: 'Max' })}: {formatNumber(maxLength)}</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Progress
|
||||
value={percentage}
|
||||
className={cn(
|
||||
'h-3',
|
||||
isOverLimit && 'bg-destructive/20',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-100 dark:bg-yellow-900/30'
|
||||
)}
|
||||
indicatorClassName={cn(
|
||||
isOverLimit && 'bg-destructive',
|
||||
!isOverLimit && isOverWarning && 'bg-yellow-500'
|
||||
)}
|
||||
/>
|
||||
{/* Warning threshold marker */}
|
||||
<div
|
||||
className="absolute top-0 h-3 flex flex-col items-center"
|
||||
style={{
|
||||
left: `${Math.min(100, (warnThreshold / maxLength) * 100)}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
<AlertTriangle className="h-3 w-3 text-yellow-500 -mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -791,6 +821,59 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Command Preview Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Eye className="h-5 w-5" />
|
||||
{formatMessage({ id: 'specs.injection.commandPreview', defaultMessage: 'Command Injection Preview' })}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{formatMessage({
|
||||
id: 'specs.injection.commandPreviewDesc',
|
||||
defaultMessage: 'Preview the content that would be injected by different CLI commands',
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
{COMMAND_PREVIEWS.map((cmd) => (
|
||||
<Button
|
||||
key={cmd.command}
|
||||
variant="outline"
|
||||
className="h-auto flex-col items-start py-3 px-4"
|
||||
onClick={() => loadCommandPreview(cmd)}
|
||||
disabled={commandPreviewLoading}
|
||||
>
|
||||
<div className="font-medium text-sm">
|
||||
{formatMessage({
|
||||
id: `specs.commandPreview.${cmd.labelKey}.label`,
|
||||
defaultMessage: cmd.labelKey
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||
{formatMessage({
|
||||
id: `specs.commandPreview.${cmd.descriptionKey}.description`,
|
||||
defaultMessage: cmd.descriptionKey
|
||||
})}
|
||||
</div>
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded mt-2 w-full text-center">
|
||||
{cmd.command.replace('ccw spec load', '').trim() || 'default'}
|
||||
</code>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{commandPreviewLoading && (
|
||||
<div className="flex items-center justify-center py-4 mt-4 border rounded-lg">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground mr-2" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.injection.loadingPreview', defaultMessage: 'Loading preview...' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Settings Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -814,39 +897,83 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
{/* Max Injection Length */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxLength">
|
||||
{formatMessage({ id: 'specs.injection.maxLength', defaultMessage: 'Max Injection Length (characters)' })}
|
||||
{formatMessage({ id: 'specs.injection.maxLength', defaultMessage: 'Max Injection Length' })}
|
||||
</Label>
|
||||
<Input
|
||||
id="maxLength"
|
||||
type="number"
|
||||
min={1000}
|
||||
max={50000}
|
||||
step={500}
|
||||
value={formData.maxLength}
|
||||
onChange={(e) => handleFieldChange('maxLength', Number(e.target.value))}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="maxLength"
|
||||
type="number"
|
||||
min={1000}
|
||||
max={50000}
|
||||
step={500}
|
||||
value={formData.maxLength}
|
||||
onChange={(e) => handleFieldChange('maxLength', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
(~{Math.ceil(formData.maxLength / 80)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })})
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({
|
||||
id: 'specs.injection.maxLengthHelp',
|
||||
defaultMessage: 'Recommended: 4000-10000. Too large may consume too much context; too small may truncate important specs.',
|
||||
defaultMessage: 'Recommended: 4000-10000 characters (50-125 lines). Too large may consume too much context.',
|
||||
})}
|
||||
</p>
|
||||
{/* Quick presets */}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.injection.quickPresets', defaultMessage: 'Quick presets:' })}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => handleFieldChange('maxLength', 4000)}
|
||||
>
|
||||
4000 (50 lines)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => handleFieldChange('maxLength', 8000)}
|
||||
>
|
||||
8000 (100 lines)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => handleFieldChange('maxLength', 12000)}
|
||||
>
|
||||
12000 (150 lines)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Warning Threshold */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="warnThreshold">
|
||||
{formatMessage({ id: 'specs.injection.warnThresholdLabel', defaultMessage: 'Warning Threshold (characters)' })}
|
||||
{formatMessage({ id: 'specs.injection.warnThresholdLabel', defaultMessage: 'Warning Threshold' })}
|
||||
</Label>
|
||||
<Input
|
||||
id="warnThreshold"
|
||||
type="number"
|
||||
min={500}
|
||||
max={formData.maxLength - 1}
|
||||
step={500}
|
||||
value={formData.warnThreshold}
|
||||
onChange={(e) => handleFieldChange('warnThreshold', Number(e.target.value))}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="warnThreshold"
|
||||
type="number"
|
||||
min={500}
|
||||
max={formData.maxLength - 1}
|
||||
step={500}
|
||||
value={formData.warnThreshold}
|
||||
onChange={(e) => handleFieldChange('warnThreshold', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
(~{Math.ceil(formData.warnThreshold / 80)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })})
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({
|
||||
id: 'specs.injection.warnThresholdHelp',
|
||||
@@ -916,6 +1043,84 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Command Preview Dialog */}
|
||||
<Dialog open={commandPreviewDialogOpen} onOpenChange={setCommandPreviewDialogOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[85vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Eye className="h-5 w-5" />
|
||||
{formatMessage({ id: 'specs.injection.previewTitle', defaultMessage: 'Injection Preview' })}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<code className="bg-muted px-2 py-1 rounded text-xs">{selectedCommand.command}</code>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{formatMessage({
|
||||
id: `specs.commandPreview.${selectedCommand.descriptionKey}.description`,
|
||||
defaultMessage: selectedCommand.descriptionKey
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{/* Stats */}
|
||||
{commandPreviewData && (
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<Badge variant="secondary">
|
||||
{commandPreviewData.stats.count} {formatMessage({ id: 'specs.injection.files', defaultMessage: 'files' })}
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
{formatNumber(commandPreviewData.stats.totalLength)} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
~{Math.ceil(commandPreviewData.stats.totalLength / 80)} {formatMessage({ id: 'specs.injection.lines', defaultMessage: 'lines' })}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{/* Preview Content */}
|
||||
<div className="flex-1 overflow-auto max-h-[60vh] border rounded-lg">
|
||||
{commandPreviewData?.files.length ? (
|
||||
<div className="space-y-4 p-4">
|
||||
{commandPreviewData.files.map((file, idx) => (
|
||||
<div key={file.file} className="space-y-2">
|
||||
{/* File Header */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{idx + 1}
|
||||
</Badge>
|
||||
<span className="font-medium">{file.title}</span>
|
||||
{file.category && (
|
||||
<Badge variant="outline" className={cn(
|
||||
'text-xs',
|
||||
CATEGORY_CONFIG[file.category as SpecCategory]?.bgColor,
|
||||
CATEGORY_CONFIG[file.category as SpecCategory]?.color
|
||||
)}>
|
||||
{file.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatNumber(file.contentLength)} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
|
||||
</span>
|
||||
</div>
|
||||
{/* File Content */}
|
||||
<pre className="text-xs whitespace-pre-wrap p-3 bg-muted rounded border-l-2 border-primary/30">
|
||||
{file.content || formatMessage({ id: 'specs.content.noContent', defaultMessage: 'No content available' })}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.injection.noFiles', defaultMessage: 'No files match this command' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user