fix: hide partial tags in AI Assist and add status feedback UI
- Refined regex to hide partial [AGENT] and [PREVIEW] tags during streaming - Added 'Neural Link Thinking' indicator for initial response phase - Added dynamic status badges (e.g., 'Generating web artifact...') during stream - Improved robustness of streaming content parser
This commit is contained in:
@@ -144,12 +144,25 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
|
|||||||
|
|
||||||
LiveCanvas.displayName = "LiveCanvas";
|
LiveCanvas.displayName = "LiveCanvas";
|
||||||
|
|
||||||
|
const ThinkingIndicator = () => (
|
||||||
|
<div className="flex items-center gap-1.5 px-4 py-3 bg-white dark:bg-[#0f1a1a]/80 border border-blue-100/70 dark:border-blue-900/50 rounded-2xl rounded-tl-none shadow-sm backdrop-blur-xl animate-in fade-in duration-300">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.3s]" />
|
||||||
|
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce [animation-delay:-0.15s]" />
|
||||||
|
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-black text-blue-700/60 dark:text-blue-200/60 uppercase tracking-widest ml-2">Neural Link Thinking...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
function parseStreamingContent(text: string) {
|
function parseStreamingContent(text: string) {
|
||||||
let agent = "general";
|
let agent = "general";
|
||||||
let preview: PreviewData | null = null;
|
let preview: PreviewData | null = null;
|
||||||
let chatDisplay = text.trim();
|
let chatDisplay = text.trim();
|
||||||
|
let status: string | null = null;
|
||||||
|
|
||||||
const decodeHtml = (value: string) => value
|
const decodeHtml = (value: string) => value
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">")
|
.replace(/>/g, ">")
|
||||||
@@ -161,27 +174,6 @@ function parseStreamingContent(text: string) {
|
|||||||
return fenced ? fenced[1].trim() : value.trim();
|
return fenced ? fenced[1].trim() : value.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
const jsonCandidate = text.trim();
|
|
||||||
if (jsonCandidate.startsWith("{") && jsonCandidate.endsWith("}")) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(jsonCandidate);
|
|
||||||
if (parsed?.agent) agent = parsed.agent;
|
|
||||||
if (parsed?.preview?.data) {
|
|
||||||
preview = {
|
|
||||||
type: parsed.preview.type || "web",
|
|
||||||
language: parsed.preview.language || "text",
|
|
||||||
data: parsed.preview.data,
|
|
||||||
isStreaming: !text.includes("[/PREVIEW]")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (typeof parsed?.content === "string") {
|
|
||||||
chatDisplay = parsed.content.trim();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore malformed JSON during stream
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentMatch = text.match(/\[AGENT:([\w-]+)\]/);
|
const agentMatch = text.match(/\[AGENT:([\w-]+)\]/);
|
||||||
if (agentMatch) agent = agentMatch[1];
|
if (agentMatch) agent = agentMatch[1];
|
||||||
|
|
||||||
@@ -193,14 +185,17 @@ function parseStreamingContent(text: string) {
|
|||||||
data: previewMatch[3].trim(),
|
data: previewMatch[3].trim(),
|
||||||
isStreaming: !text.includes("[/PREVIEW]")
|
isStreaming: !text.includes("[/PREVIEW]")
|
||||||
};
|
};
|
||||||
|
if (preview.isStreaming) {
|
||||||
|
status = `Generating ${preview.type} artifact...`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\[AGENT:|\[PREVIEW:/.test(text)) {
|
// Hide tags and partial tags from display
|
||||||
chatDisplay = text
|
chatDisplay = text
|
||||||
.replace(/\[AGENT:[\w-]+\]/g, "")
|
.replace(/\[AGENT:[\w-]+\]/g, "")
|
||||||
.replace(/\[PREVIEW:[\w-]+:?[\w-]+?\][\s\S]*?(?:\[\/PREVIEW\]|$)/g, "")
|
.replace(/\[PREVIEW:[\w-]+:?[\w-]+?\][\s\S]*?(?:\[\/PREVIEW\]|$)/g, "")
|
||||||
|
.replace(/\[(AGENT|PREVIEW)?(?::[\w-]*)?$/g, "") // Hide partial tags at the end
|
||||||
.trim();
|
.trim();
|
||||||
}
|
|
||||||
|
|
||||||
if (!preview) {
|
if (!preview) {
|
||||||
const fenced = text.match(/```(html|css|javascript|tsx|jsx|md|markdown)\s*([\s\S]*?)```/i);
|
const fenced = text.match(/```(html|css|javascript|tsx|jsx|md|markdown)\s*([\s\S]*?)```/i);
|
||||||
@@ -238,11 +233,11 @@ function parseStreamingContent(text: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatDisplay && preview) {
|
if (!chatDisplay && preview && preview.isStreaming) {
|
||||||
chatDisplay = `Rendering live artifact...`;
|
chatDisplay = `Rendering live artifact...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { chatDisplay, preview, agent };
|
return { chatDisplay, preview, agent, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Main Component ---
|
// --- Main Component ---
|
||||||
@@ -271,6 +266,8 @@ export default function AIAssist() {
|
|||||||
const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle");
|
const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle");
|
||||||
const [aiPlan, setAiPlan] = useState<any>(null);
|
const [aiPlan, setAiPlan] = useState<any>(null);
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<string | null>(null);
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const isPreviewRenderable = (preview?: PreviewData | null) => {
|
const isPreviewRenderable = (preview?: PreviewData | null) => {
|
||||||
if (!preview) return false;
|
if (!preview) return false;
|
||||||
@@ -354,7 +351,9 @@ export default function AIAssist() {
|
|||||||
currentAgent,
|
currentAgent,
|
||||||
onChunk: (chunk) => {
|
onChunk: (chunk) => {
|
||||||
accumulated += chunk;
|
accumulated += chunk;
|
||||||
const { chatDisplay, preview, agent } = parseStreamingContent(accumulated);
|
const { chatDisplay, preview, agent, status: streamStatus } = parseStreamingContent(accumulated);
|
||||||
|
|
||||||
|
if (streamStatus) setStatus(streamStatus);
|
||||||
|
|
||||||
// If we're in planning mode and see JSON, try to parse the plan
|
// If we're in planning mode and see JSON, try to parse the plan
|
||||||
if (assistStep === "plan" || assistStep === "idle") {
|
if (assistStep === "plan" || assistStep === "idle") {
|
||||||
@@ -415,6 +414,7 @@ export default function AIAssist() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
setAbortController(null);
|
setAbortController(null);
|
||||||
|
setStatus(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -627,6 +627,19 @@ export default function AIAssist() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{msg.role === "assistant" && isProcessing && i === aiAssistHistory.length - 1 && status && (
|
||||||
|
<div className="flex items-center gap-3 px-4 py-2 bg-blue-500/5 dark:bg-blue-500/10 border border-blue-500/20 rounded-2xl animate-in slide-in-from-left-2 duration-300">
|
||||||
|
<div className="relative h-2 w-2">
|
||||||
|
<div className="absolute inset-0 bg-blue-500 rounded-full animate-ping" />
|
||||||
|
<div className="absolute inset-0 bg-blue-500 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-black text-blue-600 dark:text-blue-400 uppercase tracking-[0.15em]">
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 px-2">
|
<div className="flex items-center gap-2 px-2">
|
||||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">
|
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">
|
||||||
{msg.role === "assistant" ? `Agent ${msg.agent || 'core'}` : 'Explorer'}
|
{msg.role === "assistant" ? `Agent ${msg.agent || 'core'}` : 'Explorer'}
|
||||||
@@ -634,6 +647,12 @@ export default function AIAssist() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{isProcessing && aiAssistHistory[aiAssistHistory.length - 1]?.role === "user" && (
|
||||||
|
<div className="flex flex-col items-start gap-3 animate-in fade-in duration-300">
|
||||||
|
<ThinkingIndicator />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input Area */}
|
{/* Input Area */}
|
||||||
|
|||||||
Reference in New Issue
Block a user