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:
catlog22
2026-01-31 15:27:12 +08:00
parent 4e009bb03a
commit 715ef12c92
163 changed files with 19495 additions and 715 deletions

View 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;