mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat(a2ui): Implement A2UI backend with question handling and WebSocket support
- Added A2UITypes for defining question structures and answers. - Created A2UIWebSocketHandler for managing WebSocket connections and message handling. - Developed ask-question tool for interactive user questions via A2UI. - Introduced platformUtils for platform detection and shell command handling. - Centralized TypeScript types in index.ts for better organization. - Implemented compatibility checks for hook templates based on platform requirements.
This commit is contained in:
204
ccw/frontend/src/components/shared/PromptCard.tsx
Normal file
204
ccw/frontend/src/components/shared/PromptCard.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
// ========================================
|
||||
// PromptCard Component
|
||||
// ========================================
|
||||
// Card component for displaying prompt history items
|
||||
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
Copy,
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
Tag,
|
||||
Calendar,
|
||||
} from 'lucide-react';
|
||||
import type { Prompt } from '@/types/store';
|
||||
|
||||
export interface PromptCardProps {
|
||||
/** Prompt data */
|
||||
prompt: Prompt;
|
||||
/** Called when delete action is triggered */
|
||||
onDelete?: (id: string) => void;
|
||||
/** Optional className */
|
||||
className?: string;
|
||||
/** Disabled state for actions */
|
||||
actionsDisabled?: boolean;
|
||||
/** Default expanded state */
|
||||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to readable string
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format content length
|
||||
*/
|
||||
function formatContentLength(length: number): string {
|
||||
if (length >= 1000) {
|
||||
return `${(length / 1000).toFixed(1)}k chars`;
|
||||
}
|
||||
return `${length} chars`;
|
||||
}
|
||||
|
||||
/**
|
||||
* PromptCard component for displaying prompt history items
|
||||
*/
|
||||
export function PromptCard({
|
||||
prompt,
|
||||
onDelete,
|
||||
className,
|
||||
actionsDisabled = false,
|
||||
defaultExpanded = false,
|
||||
}: PromptCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [expanded, setExpanded] = React.useState(defaultExpanded);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await navigator.clipboard.writeText(prompt.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
console.error('Failed to copy prompt');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDelete?.(prompt.id);
|
||||
};
|
||||
|
||||
const toggleExpanded = () => {
|
||||
setExpanded((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cn('transition-all duration-200', className)}>
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
{/* Title and metadata */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-sm font-medium text-foreground truncate">
|
||||
{prompt.title || formatMessage({ id: 'prompts.card.untitled' })}
|
||||
</h3>
|
||||
{prompt.category && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{prompt.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{formatDate(prompt.createdAt)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Tag className="h-3 w-3" />
|
||||
{formatContentLength(prompt.content.length)}
|
||||
</span>
|
||||
{prompt.useCount !== undefined && prompt.useCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{formatMessage({ id: 'prompts.card.used' }, { count: prompt.useCount })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{prompt.tags && prompt.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{prompt.tags.slice(0, 3).map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{prompt.tags.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{prompt.tags.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={handleCopy}
|
||||
disabled={actionsDisabled}
|
||||
title={formatMessage({ id: 'prompts.actions.copy' })}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span className="sr-only">{formatMessage({ id: 'prompts.actions.copy' })}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={actionsDisabled}
|
||||
title={formatMessage({ id: 'prompts.actions.delete' })}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="sr-only">{formatMessage({ id: 'prompts.actions.delete' })}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={toggleExpanded}
|
||||
title={expanded ? formatMessage({ id: 'prompts.actions.collapse' }) : formatMessage({ id: 'prompts.actions.expand' })}
|
||||
>
|
||||
{expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
<span className="sr-only">{expanded ? 'Collapse' : 'Expand'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{copied && (
|
||||
<p className="text-xs text-success mt-2">
|
||||
{formatMessage({ id: 'prompts.actions.copied' })}
|
||||
</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
{/* Expanded content */}
|
||||
{expanded && (
|
||||
<CardContent className="px-4 pb-4 pt-0">
|
||||
<div className="rounded-lg bg-muted/50 p-3">
|
||||
<pre className="text-sm whitespace-pre-wrap break-words text-foreground">
|
||||
{prompt.content}
|
||||
</pre>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default PromptCard;
|
||||
Reference in New Issue
Block a user