mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: Refactor AppShell and Header components, enhance memory management UI, and align API endpoints
- Removed defaultCollapsed prop from AppShell and set sidebar to be collapsed by default. - Updated Header component to remove mobile menu toggle and added a history entry button. - Refactored ExplorationsSection to streamline data extraction and improve UI rendering. - Added new messages for GitHub sync success and error handling in issues. - Implemented View Memory Dialog for better memory content viewing and editing. - Enhanced FlowToolbar to auto-create flows and improved save functionality. - Conducted a frontend audit for API endpoint alignment with backend implementations.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertCircle,
|
||||
Plus,
|
||||
@@ -24,6 +25,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
|
||||
import { IssueCard } from '@/components/shared/IssueCard';
|
||||
import { useIssues, useIssueMutations } from '@/hooks';
|
||||
import { pullIssuesFromGitHub } from '@/lib/api';
|
||||
import type { Issue } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -316,6 +318,7 @@ export function IssueManagerPage() {
|
||||
const [isNewIssueOpen, setIsNewIssueOpen] = useState(false);
|
||||
const [isEditIssueOpen, setIsEditIssueOpen] = useState(false);
|
||||
const [editingIssue, setEditingIssue] = useState<Issue | null>(null);
|
||||
const [isGithubSyncing, setIsGithubSyncing] = useState(false);
|
||||
|
||||
const {
|
||||
issues,
|
||||
@@ -378,6 +381,20 @@ export function IssueManagerPage() {
|
||||
await updateIssue(issue.id, { status });
|
||||
};
|
||||
|
||||
const handleGithubSync = async () => {
|
||||
setIsGithubSyncing(true);
|
||||
try {
|
||||
const result = await pullIssuesFromGitHub({ state: 'open', limit: 100 });
|
||||
await refetch();
|
||||
toast.success(formatMessage({ id: 'issues.messages.githubSyncSuccess' }, result));
|
||||
} catch (err) {
|
||||
console.error('GitHub sync failed:', err);
|
||||
toast.error(formatMessage({ id: 'issues.messages.githubSyncError' }));
|
||||
} finally {
|
||||
setIsGithubSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
@@ -396,8 +413,12 @@ export function IssueManagerPage() {
|
||||
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
||||
{formatMessage({ id: 'common.actions.refresh' })}
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
<Button variant="outline" onClick={handleGithubSync} disabled={isGithubSyncing}>
|
||||
{isGithubSyncing ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{formatMessage({ id: 'issues.actions.github' })}
|
||||
</Button>
|
||||
<Button onClick={() => setIsNewIssueOpen(true)}>
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
Tag,
|
||||
Loader2,
|
||||
Copy,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Star,
|
||||
Archive,
|
||||
ArchiveRestore,
|
||||
@@ -40,8 +38,7 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
interface MemoryCardProps {
|
||||
memory: CoreMemory;
|
||||
isExpanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
onView: (memory: CoreMemory) => void;
|
||||
onEdit: (memory: CoreMemory) => void;
|
||||
onDelete: (memory: CoreMemory) => void;
|
||||
onCopy: (content: string) => void;
|
||||
@@ -50,7 +47,7 @@ interface MemoryCardProps {
|
||||
onUnarchive: (memory: CoreMemory) => void;
|
||||
}
|
||||
|
||||
function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCopy, onToggleFavorite, onArchive, onUnarchive }: MemoryCardProps) {
|
||||
function MemoryCard({ memory, onView, onEdit, onDelete, onCopy, onToggleFavorite, onArchive, onUnarchive }: MemoryCardProps) {
|
||||
const formattedDate = new Date(memory.createdAt).toLocaleDateString();
|
||||
|
||||
// Parse metadata from memory
|
||||
@@ -66,10 +63,9 @@ function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCo
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
{/* Header */}
|
||||
<div
|
||||
className="p-4 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={onToggleExpand}
|
||||
onClick={() => onView(memory)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -172,20 +168,13 @@ function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCo
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
{!isExpanded && (
|
||||
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
|
||||
{memory.content}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
|
||||
{memory.content}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
{memory.tags && memory.tags.length > 0 && (
|
||||
@@ -204,16 +193,90 @@ function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCo
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-border p-4 bg-muted/30">
|
||||
<pre className="text-sm text-foreground whitespace-pre-wrap font-mono bg-background p-4 rounded-lg overflow-x-auto max-h-96">
|
||||
// ========== View Memory Dialog ==========
|
||||
|
||||
interface ViewMemoryDialogProps {
|
||||
memory: CoreMemory | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onEdit: (memory: CoreMemory) => void;
|
||||
onCopy: (content: string) => void;
|
||||
}
|
||||
|
||||
function ViewMemoryDialog({ memory, open, onOpenChange, onEdit, onCopy }: ViewMemoryDialogProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
if (!memory) return null;
|
||||
|
||||
const metadata = memory.metadata ? (typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata) : {};
|
||||
const priority = metadata.priority || 'medium';
|
||||
const formattedDate = new Date(memory.createdAt).toLocaleDateString();
|
||||
const formattedSize = memory.size
|
||||
? memory.size < 1024
|
||||
? `${memory.size} B`
|
||||
: `${(memory.size / 1024).toFixed(1)} KB`
|
||||
: 'Unknown';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Brain className="w-5 h-5 text-primary" />
|
||||
{memory.id}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{memory.source && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{memory.source}
|
||||
</Badge>
|
||||
)}
|
||||
{priority !== 'medium' && (
|
||||
<Badge variant={priority === 'high' ? 'destructive' : 'secondary'} className="text-xs">
|
||||
{priority}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formattedDate} - {formattedSize}
|
||||
</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Tags */}
|
||||
{memory.tags && memory.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{memory.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
<Tag className="w-3 h-3 mr-1" />
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto mt-2">
|
||||
<pre className="text-sm text-foreground whitespace-pre-wrap font-mono bg-muted/30 p-4 rounded-lg">
|
||||
{memory.content}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-border">
|
||||
<Button variant="outline" size="sm" onClick={() => onCopy(memory.content)}>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.actions.copy' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => { onOpenChange(false); onEdit(memory); }}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.actions.edit' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -381,7 +444,7 @@ export function MemoryPage() {
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [isNewMemoryOpen, setIsNewMemoryOpen] = useState(false);
|
||||
const [editingMemory, setEditingMemory] = useState<CoreMemory | null>(null);
|
||||
const [expandedMemories, setExpandedMemories] = useState<Set<string>>(new Set());
|
||||
const [viewingMemory, setViewingMemory] = useState<CoreMemory | null>(null);
|
||||
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived'>('memories');
|
||||
|
||||
// Build filter based on current tab
|
||||
@@ -409,18 +472,6 @@ export function MemoryPage() {
|
||||
const { createMemory, updateMemory, deleteMemory, archiveMemory, unarchiveMemory, isCreating, isUpdating } =
|
||||
useMemoryMutations();
|
||||
|
||||
const toggleExpand = (memoryId: string) => {
|
||||
setExpandedMemories((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(memoryId)) {
|
||||
next.delete(memoryId);
|
||||
} else {
|
||||
next.add(memoryId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateMemory = async (data: { content: string; tags?: string[]; metadata?: Record<string, any> }) => {
|
||||
if (editingMemory) {
|
||||
await updateMemory(editingMemory.id, data);
|
||||
@@ -673,8 +724,7 @@ export function MemoryPage() {
|
||||
<MemoryCard
|
||||
key={memory.id}
|
||||
memory={memory}
|
||||
isExpanded={expandedMemories.has(memory.id)}
|
||||
onToggleExpand={() => toggleExpand(memory.id)}
|
||||
onView={setViewingMemory}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onCopy={copyToClipboard}
|
||||
@@ -686,6 +736,15 @@ export function MemoryPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View Memory Dialog */}
|
||||
<ViewMemoryDialog
|
||||
memory={viewingMemory}
|
||||
open={viewingMemory !== null}
|
||||
onOpenChange={(open) => { if (!open) setViewingMemory(null); }}
|
||||
onEdit={handleEdit}
|
||||
onCopy={copyToClipboard}
|
||||
/>
|
||||
|
||||
{/* New/Edit Memory Dialog */}
|
||||
<NewMemoryDialog
|
||||
open={isNewMemoryOpen}
|
||||
|
||||
@@ -72,25 +72,37 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
|
||||
|
||||
// Handle save
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!currentFlow) {
|
||||
toast.error(formatMessage({ id: 'orchestrator.notifications.noFlow' }), formatMessage({ id: 'orchestrator.notifications.createFlowFirst' }));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// Update flow name if changed
|
||||
if (flowName && flowName !== currentFlow.name) {
|
||||
const name = flowName.trim() || formatMessage({ id: 'orchestrator.toolbar.placeholder' });
|
||||
|
||||
// Auto-create a new flow if none exists
|
||||
if (!currentFlow) {
|
||||
const now = new Date().toISOString();
|
||||
const newFlow: Flow = {
|
||||
id: `flow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name,
|
||||
version: 1,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
nodes: useFlowStore.getState().nodes,
|
||||
edges: useFlowStore.getState().edges,
|
||||
variables: {},
|
||||
metadata: {},
|
||||
};
|
||||
useFlowStore.setState({ currentFlow: newFlow });
|
||||
} else if (flowName && flowName !== currentFlow.name) {
|
||||
// Update flow name if changed
|
||||
useFlowStore.setState((state) => ({
|
||||
currentFlow: state.currentFlow
|
||||
? { ...state.currentFlow, name: flowName }
|
||||
? { ...state.currentFlow, name }
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
const saved = await saveFlow();
|
||||
if (saved) {
|
||||
toast.success(formatMessage({ id: 'orchestrator.notifications.flowSaved' }), formatMessage({ id: 'orchestrator.notifications.savedSuccessfully' }, { name: flowName || currentFlow.name }));
|
||||
toast.success(formatMessage({ id: 'orchestrator.notifications.flowSaved' }), formatMessage({ id: 'orchestrator.notifications.savedSuccessfully' }, { name }));
|
||||
} else {
|
||||
toast.error(formatMessage({ id: 'orchestrator.notifications.saveFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotSave' }));
|
||||
}
|
||||
@@ -99,7 +111,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [currentFlow, flowName, saveFlow]);
|
||||
}, [currentFlow, flowName, saveFlow, formatMessage]);
|
||||
|
||||
// Handle load
|
||||
const handleLoad = useCallback(
|
||||
@@ -217,7 +229,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !currentFlow}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user