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:
catlog22
2026-02-07 23:15:50 +08:00
parent 82ed5054f5
commit 80ae4baea8
13 changed files with 418 additions and 195 deletions

View File

@@ -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)}>

View File

@@ -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}

View File

@@ -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" />