mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-09 22:35:54 -05:00
chore: further refinements
This commit is contained in:
@@ -8,9 +8,10 @@ import {
|
||||
MessageContent,
|
||||
MessageResponse,
|
||||
} from "@/components/ai-elements/message";
|
||||
import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { UIDataTypes, UIMessage, UITools, ToolUIPart } from "ai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks";
|
||||
import { FindAgentsTool } from "../../tools/FindAgents/FindAgents";
|
||||
import { SearchDocsTool } from "../../tools/SearchDocs/SearchDocs";
|
||||
@@ -20,6 +21,75 @@ import { ViewAgentOutputTool } from "../../tools/ViewAgentOutput/ViewAgentOutput
|
||||
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
|
||||
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workspace media support
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolve workspace:// URLs in markdown text to proxy download URLs.
|
||||
* Detects MIME type from the hash fragment (e.g. workspace://id#video/mp4)
|
||||
* and prefixes the alt text with "video:" so the custom img component can
|
||||
* render a <video> element instead.
|
||||
*/
|
||||
function resolveWorkspaceUrls(text: string): string {
|
||||
return text.replace(
|
||||
/!\[([^\]]*)\]\(workspace:\/\/([^)#\s]+)(?:#([^)\s]*))?\)/g,
|
||||
(_match, alt: string, fileId: string, mimeHint?: string) => {
|
||||
const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
|
||||
const url = `/api/proxy${apiPath}`;
|
||||
if (mimeHint?.startsWith("video/")) {
|
||||
return ``;
|
||||
}
|
||||
return ``;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom img component for Streamdown that renders <video> elements
|
||||
* for workspace video files (detected via "video:" alt-text prefix).
|
||||
* Falls back to <video> when an <img> fails to load for workspace files.
|
||||
*/
|
||||
function WorkspaceMediaImage(props: React.JSX.IntrinsicElements["img"]) {
|
||||
const { src, alt, ...rest } = props;
|
||||
const [imgFailed, setImgFailed] = useState(false);
|
||||
const isWorkspace = src?.includes("/workspace/files/") ?? false;
|
||||
|
||||
if (!src) return null;
|
||||
|
||||
if (alt?.startsWith("video:") || (imgFailed && isWorkspace)) {
|
||||
return (
|
||||
<span className="my-2 inline-block">
|
||||
<video
|
||||
controls
|
||||
className="h-auto max-w-full rounded-md border border-zinc-200"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={src} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || "Image"}
|
||||
className="h-auto max-w-full rounded-md border border-zinc-200"
|
||||
loading="lazy"
|
||||
onError={() => {
|
||||
if (isWorkspace) setImgFailed(true);
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Stable components override for Streamdown (avoids re-creating on every render). */
|
||||
const STREAMDOWN_COMPONENTS = { img: WorkspaceMediaImage };
|
||||
|
||||
const THINKING_PHRASES = [
|
||||
"Thinking...",
|
||||
"Considering this...",
|
||||
@@ -102,8 +172,11 @@ export const ChatMessagesContainer = ({
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<MessageResponse key={`${message.id}-${i}`}>
|
||||
{part.text}
|
||||
<MessageResponse
|
||||
key={`${message.id}-${i}`}
|
||||
components={STREAMDOWN_COMPONENTS}
|
||||
>
|
||||
{resolveWorkspaceUrls(part.text)}
|
||||
</MessageResponse>
|
||||
);
|
||||
case "tool-find_block":
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
.loader {
|
||||
position: relative;
|
||||
animation: rotate 1s infinite;
|
||||
}
|
||||
|
||||
.loader::before,
|
||||
.loader::after {
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
display: block;
|
||||
/* 40% of container size */
|
||||
height: 40%;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.loader::before {
|
||||
animation: ball1 1s infinite;
|
||||
background-color: #a1a1aa; /* zinc-400 */
|
||||
box-shadow: calc(var(--spacing)) 0 0 #18181b; /* zinc-900 */
|
||||
margin-bottom: calc(var(--gap));
|
||||
}
|
||||
|
||||
.loader::after {
|
||||
animation: ball2 1s infinite;
|
||||
background-color: #18181b; /* zinc-900 */
|
||||
box-shadow: calc(var(--spacing)) 0 0 #a1a1aa; /* zinc-400 */
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg) scale(0.8);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(360deg) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(720deg) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ball1 {
|
||||
0% {
|
||||
box-shadow: calc(var(--spacing)) 0 0 #18181b;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 #18181b;
|
||||
margin-bottom: 0;
|
||||
transform: translate(calc(var(--spacing) / 2), calc(var(--spacing) / 2));
|
||||
}
|
||||
100% {
|
||||
box-shadow: calc(var(--spacing)) 0 0 #18181b;
|
||||
margin-bottom: calc(var(--gap));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ball2 {
|
||||
0% {
|
||||
box-shadow: calc(var(--spacing)) 0 0 #a1a1aa;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 #a1a1aa;
|
||||
margin-top: calc(var(--ball-size) * -1);
|
||||
transform: translate(calc(var(--spacing) / 2), calc(var(--spacing) / 2));
|
||||
}
|
||||
100% {
|
||||
box-shadow: calc(var(--spacing)) 0 0 #a1a1aa;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import styles from "./OrbitLoader.module.css";
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function OrbitLoader({ size = 24, className }: Props) {
|
||||
const ballSize = Math.round(size * 0.4);
|
||||
const spacing = Math.round(size * 0.6);
|
||||
const gap = Math.round(size * 0.2);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.loader, className)}
|
||||
style={
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
"--ball-size": `${ballSize}px`,
|
||||
"--spacing": `${spacing}px`,
|
||||
"--gap": `${gap}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ProgressBar({ value, label, className }: Props) {
|
||||
const clamped = Math.min(100, Math.max(0, value));
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-1.5", className)}>
|
||||
<div className="flex items-center justify-between text-xs text-neutral-500">
|
||||
<span>{label ?? "Working on it..."}</span>
|
||||
<span>{Math.round(clamped)}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-neutral-200">
|
||||
<div
|
||||
className="h-full rounded-full bg-neutral-900 transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${clamped}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.loader {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loader::before,
|
||||
.loader::after {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
animation: ripple 2s linear infinite;
|
||||
}
|
||||
|
||||
.loader::after {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import styles from "./PulseLoader.module.css";
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PulseLoader({ size = 24, className }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.loader, className)}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +65,10 @@ export function ContentCardTitle({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Text variant="body-medium" className={cn("truncate text-zinc-800", className)}>
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className={cn("truncate text-zinc-800", className)}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
@@ -79,7 +82,10 @@ export function ContentCardSubtitle({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Text variant="small" className={cn("mt-0.5 truncate text-zinc-800", className)}>
|
||||
<Text
|
||||
variant="small"
|
||||
className={cn("mt-0.5 truncate font-mono text-zinc-800", className)}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
@@ -93,7 +99,9 @@ export function ContentCardDescription({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Text variant="small" className={cn("mt-2 text-zinc-800", className)}>{children}</Text>
|
||||
<Text variant="body" className={cn("mt-2 text-zinc-800", className)}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +116,11 @@ export function ContentMessage({
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <Text variant="body" className={cn("text-zinc-800", className)}>{children}</Text>;
|
||||
return (
|
||||
<Text variant="body" className={cn("text-zinc-800", className)}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContentHint({
|
||||
@@ -119,7 +131,7 @@ export function ContentHint({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Text variant="small" className={cn("italic text-neutral-800", className)}>
|
||||
<Text variant="small" className={cn("text-neutral-500", className)}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Hook that returns a progress value that starts fast and slows down,
|
||||
* asymptotically approaching but never reaching the max value.
|
||||
*
|
||||
* Uses a half-life formula: progress = max * (1 - 0.5^(time/halfLife))
|
||||
* This creates a "loading bar" effect where:
|
||||
* - 50% is reached at halfLifeSeconds
|
||||
* - 75% is reached at 2 * halfLifeSeconds
|
||||
* - 87.5% is reached at 3 * halfLifeSeconds
|
||||
*
|
||||
* @param isActive - Whether the progress should be animating
|
||||
* @param halfLifeSeconds - Time in seconds to reach 50% progress (default: 30)
|
||||
* @param maxProgress - Maximum progress value to approach (default: 100)
|
||||
* @param intervalMs - Update interval in milliseconds (default: 100)
|
||||
* @returns Current progress value (0–maxProgress)
|
||||
*/
|
||||
export function useAsymptoticProgress(
|
||||
isActive: boolean,
|
||||
halfLifeSeconds = 30,
|
||||
maxProgress = 100,
|
||||
intervalMs = 100,
|
||||
) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const elapsedTimeRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
setProgress(0);
|
||||
elapsedTimeRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
elapsedTimeRef.current += intervalMs / 1000;
|
||||
const newProgress =
|
||||
maxProgress *
|
||||
(1 - Math.pow(0.5, elapsedTimeRef.current / halfLifeSeconds));
|
||||
setProgress(newProgress);
|
||||
}, intervalMs);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isActive, halfLifeSeconds, maxProgress, intervalMs]);
|
||||
|
||||
return progress;
|
||||
}
|
||||
@@ -24,6 +24,29 @@ import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function slugify(text: string) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
"Messages",
|
||||
"Tool: Find Blocks",
|
||||
"Tool: Find Agents (Marketplace)",
|
||||
"Tool: Find Agents (Library)",
|
||||
"Tool: Search Docs",
|
||||
"Tool: Get Doc Page",
|
||||
"Tool: Run Block",
|
||||
"Tool: Run Agent",
|
||||
"Tool: Schedule Agent",
|
||||
"Tool: Create Agent",
|
||||
"Tool: Edit Agent",
|
||||
"Tool: View Agent Output",
|
||||
"Full Conversation Example",
|
||||
] as const;
|
||||
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
@@ -32,7 +55,7 @@ function Section({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<div id={slugify(title)} className="mb-10 scroll-mt-6">
|
||||
<h2 className="mb-4 border-b border-neutral-200 pb-2 text-lg font-semibold text-neutral-800">
|
||||
{title}
|
||||
</h2>
|
||||
@@ -74,15 +97,36 @@ function uid() {
|
||||
export default function StyleguidePage() {
|
||||
return (
|
||||
<CopilotChatActionsProvider onSend={(msg) => alert(`onSend: ${msg}`)}>
|
||||
<div className="h-[calc(100vh-72px)] overflow-y-auto bg-[#f8f8f9]">
|
||||
<div className="mx-auto max-w-3xl px-4 py-10">
|
||||
<h1 className="mb-1 text-2xl font-bold text-neutral-900">
|
||||
Copilot Styleguide
|
||||
</h1>
|
||||
<p className="mb-8 text-sm text-neutral-500">
|
||||
Static showcase of all chat message types, tool states &
|
||||
variants.
|
||||
<div className="flex h-[calc(100vh-72px)] bg-[#f8f8f9]">
|
||||
{/* Sidebar */}
|
||||
<nav className="sticky top-0 hidden h-full w-56 shrink-0 overflow-y-auto border-r border-neutral-200 bg-white px-3 py-6 lg:block">
|
||||
<p className="mb-3 px-2 text-[11px] font-semibold uppercase tracking-wider text-neutral-400">
|
||||
Sections
|
||||
</p>
|
||||
<ul className="space-y-0.5">
|
||||
{SECTIONS.map((title) => (
|
||||
<li key={title}>
|
||||
<a
|
||||
href={`#${slugify(title)}`}
|
||||
className="block rounded-md px-2 py-1.5 text-[13px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-900"
|
||||
>
|
||||
{title.replace(/^Tool: /, "")}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto max-w-3xl px-4 py-10">
|
||||
<h1 className="mb-1 text-2xl font-bold text-neutral-900">
|
||||
Copilot Styleguide
|
||||
</h1>
|
||||
<p className="mb-8 text-sm text-neutral-500">
|
||||
Static showcase of all chat message types, tool states &
|
||||
variants.
|
||||
</p>
|
||||
|
||||
{/* ============================================================= */}
|
||||
{/* MESSAGE TYPES */}
|
||||
@@ -566,6 +610,131 @@ export default function StyleguidePage() {
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output available (JSON object output — interactive viewer)">
|
||||
<RunBlockTool
|
||||
part={{
|
||||
type: "tool-run_block",
|
||||
toolCallId: uid(),
|
||||
state: "output-available",
|
||||
input: { block_id: "api-block-456" },
|
||||
output: {
|
||||
type: ResponseType.block_output,
|
||||
block_id: "api-block-456",
|
||||
block_name: "API Request",
|
||||
message: "Successfully fetched user profile data.",
|
||||
outputs: {
|
||||
response: [
|
||||
{
|
||||
id: 42,
|
||||
name: "Jane Doe",
|
||||
email: "jane@example.com",
|
||||
roles: ["admin", "editor"],
|
||||
settings: {
|
||||
theme: "dark",
|
||||
notifications: true,
|
||||
language: "en",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output available (image URL — ImageRenderer)">
|
||||
<RunBlockTool
|
||||
part={{
|
||||
type: "tool-run_block",
|
||||
toolCallId: uid(),
|
||||
state: "output-available",
|
||||
input: { block_id: "image-gen-789" },
|
||||
output: {
|
||||
type: ResponseType.block_output,
|
||||
block_id: "image-gen-789",
|
||||
block_name: "Image Generator",
|
||||
message: "Generated image successfully.",
|
||||
outputs: {
|
||||
image: [
|
||||
"https://picsum.photos/seed/styleguide/600/400",
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output available (markdown text — MarkdownRenderer)">
|
||||
<RunBlockTool
|
||||
part={{
|
||||
type: "tool-run_block",
|
||||
toolCallId: uid(),
|
||||
state: "output-available",
|
||||
input: { block_id: "summarizer-101" },
|
||||
output: {
|
||||
type: ResponseType.block_output,
|
||||
block_id: "summarizer-101",
|
||||
block_name: "Text Summarizer",
|
||||
message: "Document summarized successfully.",
|
||||
outputs: {
|
||||
summary: [
|
||||
"## Executive Summary\n\nThe quarterly report shows **strong growth** across all departments:\n\n- Revenue increased by *23%* compared to Q3\n- Customer satisfaction score: `4.8/5.0`\n- New user sign-ups doubled\n\n### Key Takeaways\n\n1. **Product launches** drove the majority of growth\n2. **Marketing campaigns** exceeded ROI targets\n3. **Infrastructure costs** remained flat despite scaling\n\n> Overall, this was our strongest quarter to date.\n\n| Metric | Q3 | Q4 | Change |\n|--------|-----|-----|--------|\n| Revenue | $2.1M | $2.6M | +23% |\n| Users | 10k | 20k | +100% |\n| NPS | 72 | 78 | +6 |",
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output available (plain text — TextRenderer)">
|
||||
<RunBlockTool
|
||||
part={{
|
||||
type: "tool-run_block",
|
||||
toolCallId: uid(),
|
||||
state: "output-available",
|
||||
input: { block_id: "translate-202" },
|
||||
output: {
|
||||
type: ResponseType.block_output,
|
||||
block_id: "translate-202",
|
||||
block_name: "Translate Text",
|
||||
message: "Translation completed.",
|
||||
outputs: {
|
||||
translated_text: [
|
||||
"Bonjour le monde! Ceci est un exemple de texte traduit du bloc de traduction.",
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output available (multiple items with expand)">
|
||||
<RunBlockTool
|
||||
part={{
|
||||
type: "tool-run_block",
|
||||
toolCallId: uid(),
|
||||
state: "output-available",
|
||||
input: { block_id: "scraper-303" },
|
||||
output: {
|
||||
type: ResponseType.block_output,
|
||||
block_id: "scraper-303",
|
||||
block_name: "Web Scraper",
|
||||
message: "Scraped 6 articles from the feed.",
|
||||
outputs: {
|
||||
articles: [
|
||||
{ title: "AI Advances in 2026", url: "https://example.com/1", score: 142 },
|
||||
{ title: "New Framework Released", url: "https://example.com/2", score: 98 },
|
||||
{ title: "Open Source Milestone", url: "https://example.com/3", score: 87 },
|
||||
{ title: "Cloud Computing Trends", url: "https://example.com/4", score: 76 },
|
||||
{ title: "Developer Survey Results", url: "https://example.com/5", score: 64 },
|
||||
{ title: "Security Best Practices", url: "https://example.com/6", score: 51 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output error">
|
||||
<RunBlockTool
|
||||
part={{
|
||||
@@ -1111,6 +1280,57 @@ export default function StyleguidePage() {
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output available (rich outputs — JSON + markdown + image)">
|
||||
<ViewAgentOutputTool
|
||||
part={{
|
||||
type: "tool-view_agent_output",
|
||||
toolCallId: uid(),
|
||||
state: "output-available",
|
||||
input: {
|
||||
agent_name: "Research Agent",
|
||||
execution_id: "exec-rich-456",
|
||||
},
|
||||
output: {
|
||||
type: ResponseType.agent_output,
|
||||
agent_id: "agent-456",
|
||||
agent_name: "Research Agent",
|
||||
message: "Research completed with multiple output types.",
|
||||
library_agent_link: "/library/agents/lib-agent-456",
|
||||
execution: {
|
||||
execution_id: "exec-rich-456",
|
||||
status: "completed",
|
||||
inputs_summary: {
|
||||
topic: "Artificial Intelligence in Healthcare",
|
||||
depth: "comprehensive",
|
||||
format: "report",
|
||||
},
|
||||
outputs: {
|
||||
report: [
|
||||
"## AI in Healthcare: 2026 Landscape\n\n### Key Findings\n\n- **Diagnostic accuracy** improved by 34% with AI-assisted imaging\n- Drug discovery timelines reduced from *10 years to 3 years*\n- Patient outcomes improved across `87%` of pilot programs\n\n> AI is not replacing doctors — it's augmenting their capabilities.\n\n### Adoption by Region\n\n| Region | Adoption Rate | Growth |\n|--------|--------------|--------|\n| North America | 78% | +15% |\n| Europe | 62% | +22% |\n| Asia Pacific | 71% | +31% |",
|
||||
],
|
||||
metadata: [
|
||||
{
|
||||
sources_analyzed: 142,
|
||||
confidence_score: 0.94,
|
||||
processing_time_ms: 3420,
|
||||
model_version: "v2.3.1",
|
||||
categories: [
|
||||
"healthcare",
|
||||
"machine-learning",
|
||||
"diagnostics",
|
||||
],
|
||||
},
|
||||
],
|
||||
chart: [
|
||||
"https://picsum.photos/seed/chart-demo/500/300",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SubSection>
|
||||
|
||||
<SubSection label="Output available (no execution selected)">
|
||||
<ViewAgentOutputTool
|
||||
part={{
|
||||
@@ -1282,6 +1502,7 @@ export default function StyleguidePage() {
|
||||
</Conversation>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CopilotChatActionsProvider>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ClarificationQuestionsWidget,
|
||||
type ClarifyingQuestion as WidgetClarifyingQuestion,
|
||||
} from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
|
||||
import { WarningDiamondIcon } from "@phosphor-icons/react";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||
import { ProgressBar } from "../../components/ProgressBar/ProgressBar";
|
||||
import {
|
||||
ContentCardDescription,
|
||||
ContentCardSubtitle,
|
||||
ContentCodeBlock,
|
||||
ContentGrid,
|
||||
ContentHint,
|
||||
ContentLink,
|
||||
ContentMessage,
|
||||
} from "../../components/ToolAccordion/AccordionContent";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import {
|
||||
ClarificationQuestionsWidget,
|
||||
type ClarifyingQuestion as WidgetClarifyingQuestion,
|
||||
} from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import { useAsymptoticProgress } from "../../hooks/useAsymptoticProgress";
|
||||
import {
|
||||
AccordionIcon,
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getCreateAgentToolOutput,
|
||||
@@ -28,7 +32,6 @@ import {
|
||||
isOperationInProgressOutput,
|
||||
isOperationPendingOutput,
|
||||
isOperationStartedOutput,
|
||||
AccordionIcon,
|
||||
ToolIcon,
|
||||
truncateText,
|
||||
type CreateAgentToolOutput,
|
||||
@@ -48,7 +51,8 @@ interface Props {
|
||||
|
||||
function getAccordionMeta(output: CreateAgentToolOutput): {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
titleClassName?: string;
|
||||
description?: string;
|
||||
} {
|
||||
const icon = <AccordionIcon />;
|
||||
@@ -76,9 +80,16 @@ function getAccordionMeta(output: CreateAgentToolOutput): {
|
||||
isOperationPendingOutput(output) ||
|
||||
isOperationInProgressOutput(output)
|
||||
) {
|
||||
return { icon, title: "Creating agent" };
|
||||
return {
|
||||
icon: <OrbitLoader size={32} />,
|
||||
title: "Creating agent, this may take a few minutes. Sit back and relax.",
|
||||
};
|
||||
}
|
||||
return { icon, title: "Error" };
|
||||
return {
|
||||
icon: <WarningDiamondIcon size={32} weight="light" className="text-red-500" />,
|
||||
title: "Error",
|
||||
titleClassName: "text-red-500",
|
||||
};
|
||||
}
|
||||
|
||||
export function CreateAgentTool({ part }: Props) {
|
||||
@@ -90,6 +101,12 @@ export function CreateAgentTool({ part }: Props) {
|
||||
const output = getCreateAgentToolOutput(part);
|
||||
const isError =
|
||||
part.state === "output-error" || (!!output && isErrorOutput(output));
|
||||
const isOperating =
|
||||
!!output &&
|
||||
(isOperationStartedOutput(output) ||
|
||||
isOperationPendingOutput(output) ||
|
||||
isOperationInProgressOutput(output));
|
||||
const progress = useAsymptoticProgress(isOperating);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
@@ -102,12 +119,20 @@ export function CreateAgentTool({ part }: Props) {
|
||||
isErrorOutput(output));
|
||||
|
||||
function handleClarificationAnswers(answers: Record<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
.map(([keyword, answer]) => `${keyword}: ${answer}`)
|
||||
.join("\n");
|
||||
const questions =
|
||||
output && isClarificationNeededOutput(output)
|
||||
? (output.questions ?? [])
|
||||
: [];
|
||||
|
||||
const contextMessage = questions
|
||||
.map((q) => {
|
||||
const answer = answers[q.keyword] || "";
|
||||
return `> ${q.question}\n\n${answer}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
onSend(
|
||||
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
|
||||
`**Here are my answers:**\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,26 +149,13 @@ export function CreateAgentTool({ part }: Props) {
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={isClarificationNeededOutput(output)}
|
||||
defaultExpanded={isOperating || isClarificationNeededOutput(output)}
|
||||
>
|
||||
{(isOperationStartedOutput(output) ||
|
||||
isOperationPendingOutput(output)) && (
|
||||
{isOperating && (
|
||||
<ContentGrid>
|
||||
<ContentMessage>{output.message}</ContentMessage>
|
||||
<ContentCardSubtitle>
|
||||
Operation: {output.operation_id}
|
||||
</ContentCardSubtitle>
|
||||
<ProgressBar value={progress} className="max-w-[280px]" />
|
||||
<ContentHint>
|
||||
Check your library in a few minutes.
|
||||
</ContentHint>
|
||||
</ContentGrid>
|
||||
)}
|
||||
|
||||
{isOperationInProgressOutput(output) && (
|
||||
<ContentGrid>
|
||||
<ContentMessage>{output.message}</ContentMessage>
|
||||
<ContentHint>
|
||||
Please wait for the current operation to finish.
|
||||
This could take a few minutes, grab a coffee ☕
|
||||
</ContentHint>
|
||||
</ContentGrid>
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,13 @@ import type { OperationInProgressResponse } from "@/app/api/__generated__/models
|
||||
import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
|
||||
import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
|
||||
import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
||||
import { PlusCircleIcon, PlusIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
PlusCircleIcon,
|
||||
PlusIcon,
|
||||
WarningDiamondIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||
|
||||
export type CreateAgentToolOutput =
|
||||
| OperationStartedResponse
|
||||
@@ -150,19 +155,13 @@ export function ToolIcon({
|
||||
isStreaming?: boolean;
|
||||
isError?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<PlusIcon
|
||||
size={14}
|
||||
weight="regular"
|
||||
className={
|
||||
isError
|
||||
? "text-red-500"
|
||||
: isStreaming
|
||||
? "text-neutral-500"
|
||||
: "text-neutral-400"
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (isError) {
|
||||
return <WarningDiamondIcon size={14} weight="regular" className="text-red-500" />;
|
||||
}
|
||||
if (isStreaming) {
|
||||
return <OrbitLoader size={24} />;
|
||||
}
|
||||
return <PlusIcon size={14} weight="regular" className="text-neutral-400" />;
|
||||
}
|
||||
|
||||
export function AccordionIcon() {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { WarningDiamondIcon } from "@phosphor-icons/react";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||
import { ProgressBar } from "../../components/ProgressBar/ProgressBar";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import {
|
||||
ContentCardDescription,
|
||||
ContentCardSubtitle,
|
||||
ContentCodeBlock,
|
||||
ContentGrid,
|
||||
ContentHint,
|
||||
ContentLink,
|
||||
ContentMessage,
|
||||
} from "../../components/ToolAccordion/AccordionContent";
|
||||
import { useAsymptoticProgress } from "../../hooks/useAsymptoticProgress";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import {
|
||||
ClarificationQuestionsWidget,
|
||||
@@ -49,6 +52,7 @@ interface Props {
|
||||
function getAccordionMeta(output: EditAgentToolOutput): {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
titleClassName?: string;
|
||||
description?: string;
|
||||
} {
|
||||
const icon = <AccordionIcon />;
|
||||
@@ -76,9 +80,13 @@ function getAccordionMeta(output: EditAgentToolOutput): {
|
||||
isOperationPendingOutput(output) ||
|
||||
isOperationInProgressOutput(output)
|
||||
) {
|
||||
return { icon, title: "Editing agent" };
|
||||
return { icon: <OrbitLoader size={32} />, title: "Editing agent" };
|
||||
}
|
||||
return { icon, title: "Error" };
|
||||
return {
|
||||
icon: <WarningDiamondIcon size={32} weight="light" className="text-red-500" />,
|
||||
title: "Error",
|
||||
titleClassName: "text-red-500",
|
||||
};
|
||||
}
|
||||
|
||||
export function EditAgentTool({ part }: Props) {
|
||||
@@ -90,6 +98,12 @@ export function EditAgentTool({ part }: Props) {
|
||||
const output = getEditAgentToolOutput(part);
|
||||
const isError =
|
||||
part.state === "output-error" || (!!output && isErrorOutput(output));
|
||||
const isOperating =
|
||||
!!output &&
|
||||
(isOperationStartedOutput(output) ||
|
||||
isOperationPendingOutput(output) ||
|
||||
isOperationInProgressOutput(output));
|
||||
const progress = useAsymptoticProgress(isOperating);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
@@ -102,12 +116,20 @@ export function EditAgentTool({ part }: Props) {
|
||||
isErrorOutput(output));
|
||||
|
||||
function handleClarificationAnswers(answers: Record<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
.map(([keyword, answer]) => `${keyword}: ${answer}`)
|
||||
.join("\n");
|
||||
const questions =
|
||||
output && isClarificationNeededOutput(output)
|
||||
? (output.questions ?? [])
|
||||
: [];
|
||||
|
||||
const contextMessage = questions
|
||||
.map((q) => {
|
||||
const answer = answers[q.keyword] || "";
|
||||
return `> ${q.question}\n\n${answer}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
onSend(
|
||||
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with editing the agent.`,
|
||||
`**Here are my answers:**\n\n${contextMessage}\n\nPlease proceed with editing the agent.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,26 +146,13 @@ export function EditAgentTool({ part }: Props) {
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={isClarificationNeededOutput(output)}
|
||||
defaultExpanded={isOperating || isClarificationNeededOutput(output)}
|
||||
>
|
||||
{(isOperationStartedOutput(output) ||
|
||||
isOperationPendingOutput(output)) && (
|
||||
{isOperating && (
|
||||
<ContentGrid>
|
||||
<ContentMessage>{output.message}</ContentMessage>
|
||||
<ContentCardSubtitle>
|
||||
Operation: {output.operation_id}
|
||||
</ContentCardSubtitle>
|
||||
<ProgressBar value={progress} className="max-w-[280px]" />
|
||||
<ContentHint>
|
||||
Check your library in a few minutes.
|
||||
</ContentHint>
|
||||
</ContentGrid>
|
||||
)}
|
||||
|
||||
{isOperationInProgressOutput(output) && (
|
||||
<ContentGrid>
|
||||
<ContentMessage>{output.message}</ContentMessage>
|
||||
<ContentHint>
|
||||
Please wait for the current operation to finish.
|
||||
This could take a few minutes, grab a coffee ☕
|
||||
</ContentHint>
|
||||
</ContentGrid>
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,13 @@ import type { OperationInProgressResponse } from "@/app/api/__generated__/models
|
||||
import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
|
||||
import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
|
||||
import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
||||
import { NotePencilIcon, PencilLineIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
NotePencilIcon,
|
||||
PencilLineIcon,
|
||||
WarningDiamondIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||
|
||||
export type EditAgentToolOutput =
|
||||
| OperationStartedResponse
|
||||
@@ -150,19 +155,13 @@ export function ToolIcon({
|
||||
isStreaming?: boolean;
|
||||
isError?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<PencilLineIcon
|
||||
size={14}
|
||||
weight="regular"
|
||||
className={
|
||||
isError
|
||||
? "text-red-500"
|
||||
: isStreaming
|
||||
? "text-neutral-500"
|
||||
: "text-neutral-400"
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (isError) {
|
||||
return <WarningDiamondIcon size={14} weight="regular" className="text-red-500" />;
|
||||
}
|
||||
if (isStreaming) {
|
||||
return <OrbitLoader size={24} />;
|
||||
}
|
||||
return <PencilLineIcon size={14} weight="regular" className="text-neutral-400" />;
|
||||
}
|
||||
|
||||
export function AccordionIcon() {
|
||||
|
||||
@@ -64,6 +64,7 @@ export function RunAgentTool({ part }: Props) {
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={
|
||||
isRunAgentExecutionStartedOutput(output) ||
|
||||
isRunAgentSetupRequirementsOutput(output) ||
|
||||
isRunAgentAgentDetailsOutput(output)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import type { ExecutionStartedResponse } from "@/app/api/__generated__/models/executionStartedResponse";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ContentCard,
|
||||
ContentCardDescription,
|
||||
@@ -26,9 +26,8 @@ export function ExecutionStartedCard({ output }: Props) {
|
||||
<ContentCardDescription>{output.message}</ContentCardDescription>
|
||||
{output.library_agent_link && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="small"
|
||||
className="mt-3 w-full"
|
||||
className="mt-3"
|
||||
onClick={() => router.push(output.library_agent_link!)}
|
||||
>
|
||||
View Execution
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
WarningDiamondIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { PulseLoader } from "../../components/PulseLoader/PulseLoader";
|
||||
|
||||
export interface RunAgentInput {
|
||||
username_agent_slug?: string;
|
||||
@@ -167,13 +168,10 @@ export function ToolIcon({
|
||||
if (isError) {
|
||||
return <WarningDiamondIcon size={14} weight="regular" className="text-red-500" />;
|
||||
}
|
||||
return (
|
||||
<PlayIcon
|
||||
size={14}
|
||||
weight="regular"
|
||||
className={isStreaming ? "text-neutral-500" : "text-neutral-400"}
|
||||
/>
|
||||
);
|
||||
if (isStreaming) {
|
||||
return <PulseLoader size={40} className="text-neutral-700" />;
|
||||
}
|
||||
return <PlayIcon size={14} weight="regular" className="text-neutral-400" />;
|
||||
}
|
||||
|
||||
export function AccordionIcon() {
|
||||
@@ -203,7 +201,7 @@ export function getAccordionMeta(output: RunAgentToolOutput): {
|
||||
? output.status.trim()
|
||||
: "started";
|
||||
return {
|
||||
icon,
|
||||
icon: <PulseLoader size={28} className="text-neutral-700" />,
|
||||
title: output.graph_name,
|
||||
description: `Status: ${statusText}`,
|
||||
};
|
||||
|
||||
@@ -57,7 +57,10 @@ export function RunBlockTool({ part }: Props) {
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={isRunBlockSetupRequirementsOutput(output)}
|
||||
defaultExpanded={
|
||||
isRunBlockBlockOutput(output) ||
|
||||
isRunBlockSetupRequirementsOutput(output)
|
||||
}
|
||||
>
|
||||
{isRunBlockBlockOutput(output) && <BlockOutputCard output={output} />}
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ import React, { useState } from "react";
|
||||
import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import type { BlockOutputResponse } from "@/app/api/__generated__/models/blockOutputResponse";
|
||||
import {
|
||||
globalRegistry,
|
||||
OutputItem,
|
||||
} from "@/components/contextual/OutputRenderers";
|
||||
import type { OutputMetadata } from "@/components/contextual/OutputRenderers";
|
||||
import {
|
||||
ContentBadge,
|
||||
ContentCard,
|
||||
@@ -11,7 +16,6 @@ import {
|
||||
ContentGrid,
|
||||
ContentMessage,
|
||||
} from "../../../../components/ToolAccordion/AccordionContent";
|
||||
import { formatMaybeJson } from "../../helpers";
|
||||
|
||||
interface Props {
|
||||
output: BlockOutputResponse;
|
||||
@@ -19,81 +23,59 @@ interface Props {
|
||||
|
||||
const COLLAPSED_LIMIT = 3;
|
||||
|
||||
function resolveWorkspaceUrl(src: string): string {
|
||||
const withoutPrefix = src.replace("workspace://", "");
|
||||
const fileId = withoutPrefix.split("#")[0];
|
||||
const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
|
||||
return `/api/proxy${apiPath}`;
|
||||
}
|
||||
|
||||
function getWorkspaceMimeHint(src: string): string | undefined {
|
||||
const hashIndex = src.indexOf("#");
|
||||
if (hashIndex === -1) return undefined;
|
||||
return src.slice(hashIndex + 1) || undefined;
|
||||
}
|
||||
|
||||
function isWorkspaceRef(value: unknown): value is string {
|
||||
return typeof value === "string" && value.startsWith("workspace://");
|
||||
}
|
||||
|
||||
function WorkspaceMedia({ value }: { value: string }) {
|
||||
const [imgFailed, setImgFailed] = useState(false);
|
||||
const resolvedUrl = resolveWorkspaceUrl(value);
|
||||
const mime = getWorkspaceMimeHint(value);
|
||||
function resolveForRenderer(value: unknown): {
|
||||
value: unknown;
|
||||
metadata?: OutputMetadata;
|
||||
} {
|
||||
if (!isWorkspaceRef(value)) return { value };
|
||||
|
||||
if (mime?.startsWith("video/") || imgFailed) {
|
||||
const withoutPrefix = value.replace("workspace://", "");
|
||||
const fileId = withoutPrefix.split("#")[0];
|
||||
const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
|
||||
const url = `/api/proxy${apiPath}`;
|
||||
|
||||
const hashIndex = value.indexOf("#");
|
||||
const mimeHint =
|
||||
hashIndex !== -1 ? value.slice(hashIndex + 1) || undefined : undefined;
|
||||
|
||||
const metadata: OutputMetadata = {};
|
||||
if (mimeHint) {
|
||||
metadata.mimeType = mimeHint;
|
||||
if (mimeHint.startsWith("image/")) metadata.type = "image";
|
||||
else if (mimeHint.startsWith("video/")) metadata.type = "video";
|
||||
}
|
||||
|
||||
return { value: url, metadata };
|
||||
}
|
||||
|
||||
function RenderOutputValue({ value }: { value: unknown }) {
|
||||
const resolved = resolveForRenderer(value);
|
||||
const renderer = globalRegistry.getRenderer(resolved.value, resolved.metadata);
|
||||
|
||||
if (renderer) {
|
||||
return (
|
||||
<video
|
||||
controls
|
||||
className="mt-2 h-auto max-w-full rounded-md border border-zinc-200"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={resolvedUrl} />
|
||||
</video>
|
||||
<OutputItem
|
||||
value={resolved.value}
|
||||
metadata={resolved.metadata}
|
||||
renderer={renderer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mime?.startsWith("audio/")) {
|
||||
return <audio controls src={resolvedUrl} className="mt-2 w-full" />;
|
||||
// Fallback for audio workspace refs
|
||||
if (
|
||||
isWorkspaceRef(value) &&
|
||||
resolved.metadata?.mimeType?.startsWith("audio/")
|
||||
) {
|
||||
return (
|
||||
<audio controls src={String(resolved.value)} className="mt-2 w-full" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolvedUrl}
|
||||
alt="Output media"
|
||||
className="mt-2 h-auto max-w-full rounded-md border border-zinc-200"
|
||||
loading="lazy"
|
||||
onError={() => setImgFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOutputValue(value: unknown): React.ReactNode {
|
||||
if (isWorkspaceRef(value)) {
|
||||
return <WorkspaceMedia value={value} />;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const hasWorkspace = value.some(isWorkspaceRef);
|
||||
if (hasWorkspace) {
|
||||
return (
|
||||
<>
|
||||
{value.map((item, i) =>
|
||||
isWorkspaceRef(item) ? (
|
||||
<WorkspaceMedia key={i} value={item} />
|
||||
) : (
|
||||
<pre
|
||||
key={i}
|
||||
className="mt-1 whitespace-pre-wrap text-xs text-muted-foreground"
|
||||
>
|
||||
{formatMaybeJson(item)}
|
||||
</pre>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -105,7 +87,6 @@ function OutputKeySection({
|
||||
items: unknown[];
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const mediaContent = renderOutputValue(items);
|
||||
const hasMoreItems = items.length > COLLAPSED_LIMIT;
|
||||
const visibleItems = expanded ? items : items.slice(0, COLLAPSED_LIMIT);
|
||||
|
||||
@@ -117,12 +98,12 @@ function OutputKeySection({
|
||||
{items.length} item{items.length === 1 ? "" : "s"}
|
||||
</ContentBadge>
|
||||
</div>
|
||||
{mediaContent || (
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(visibleItems)}
|
||||
</pre>
|
||||
)}
|
||||
{!mediaContent && hasMoreItems && (
|
||||
<div className="mt-2">
|
||||
{visibleItems.map((item, i) => (
|
||||
<RenderOutputValue key={i} value={item} />
|
||||
))}
|
||||
</div>
|
||||
{hasMoreItems && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
WarningDiamondIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { PulseLoader } from "../../components/PulseLoader/PulseLoader";
|
||||
|
||||
export interface RunBlockInput {
|
||||
block_id?: string;
|
||||
@@ -116,13 +117,10 @@ export function ToolIcon({
|
||||
if (isError) {
|
||||
return <WarningDiamondIcon size={14} weight="regular" className="text-red-500" />;
|
||||
}
|
||||
return (
|
||||
<PlayIcon
|
||||
size={14}
|
||||
weight="regular"
|
||||
className={isStreaming ? "text-neutral-500" : "text-neutral-400"}
|
||||
/>
|
||||
);
|
||||
if (isStreaming) {
|
||||
return <PulseLoader size={40} className="text-neutral-700" />;
|
||||
}
|
||||
return <PlayIcon size={14} weight="regular" className="text-neutral-400" />;
|
||||
}
|
||||
|
||||
export function AccordionIcon() {
|
||||
@@ -149,7 +147,7 @@ export function getAccordionMeta(output: RunBlockToolOutput): {
|
||||
if (isRunBlockBlockOutput(output)) {
|
||||
const keys = Object.keys(output.outputs ?? {});
|
||||
return {
|
||||
icon,
|
||||
icon: <PulseLoader size={32} className="text-neutral-700" />,
|
||||
title: output.block_name,
|
||||
description:
|
||||
keys.length > 0
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import type { ToolUIPart } from "ai";
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace";
|
||||
import {
|
||||
globalRegistry,
|
||||
OutputItem,
|
||||
} from "@/components/contextual/OutputRenderers";
|
||||
import type { OutputMetadata } from "@/components/contextual/OutputRenderers";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import {
|
||||
@@ -41,6 +46,65 @@ interface Props {
|
||||
part: ViewAgentOutputToolPart;
|
||||
}
|
||||
|
||||
function isWorkspaceRef(value: unknown): value is string {
|
||||
return typeof value === "string" && value.startsWith("workspace://");
|
||||
}
|
||||
|
||||
function resolveForRenderer(value: unknown): {
|
||||
value: unknown;
|
||||
metadata?: OutputMetadata;
|
||||
} {
|
||||
if (!isWorkspaceRef(value)) return { value };
|
||||
|
||||
const withoutPrefix = value.replace("workspace://", "");
|
||||
const fileId = withoutPrefix.split("#")[0];
|
||||
const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
|
||||
const url = `/api/proxy${apiPath}`;
|
||||
|
||||
const hashIndex = value.indexOf("#");
|
||||
const mimeHint =
|
||||
hashIndex !== -1 ? value.slice(hashIndex + 1) || undefined : undefined;
|
||||
|
||||
const metadata: OutputMetadata = {};
|
||||
if (mimeHint) {
|
||||
metadata.mimeType = mimeHint;
|
||||
if (mimeHint.startsWith("image/")) metadata.type = "image";
|
||||
else if (mimeHint.startsWith("video/")) metadata.type = "video";
|
||||
}
|
||||
|
||||
return { value: url, metadata };
|
||||
}
|
||||
|
||||
function RenderOutputValue({ value }: { value: unknown }) {
|
||||
const resolved = resolveForRenderer(value);
|
||||
const renderer = globalRegistry.getRenderer(
|
||||
resolved.value,
|
||||
resolved.metadata,
|
||||
);
|
||||
|
||||
if (renderer) {
|
||||
return (
|
||||
<OutputItem
|
||||
value={resolved.value}
|
||||
metadata={resolved.metadata}
|
||||
renderer={renderer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback for audio workspace refs
|
||||
if (
|
||||
isWorkspaceRef(value) &&
|
||||
resolved.metadata?.mimeType?.startsWith("audio/")
|
||||
) {
|
||||
return (
|
||||
<audio controls src={String(resolved.value)} className="mt-2 w-full" />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAccordionMeta(output: ViewAgentOutputToolOutput): {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
@@ -62,87 +126,6 @@ function getAccordionMeta(output: ViewAgentOutputToolOutput): {
|
||||
return { icon, title: "Error" };
|
||||
}
|
||||
|
||||
function resolveWorkspaceUrl(src: string): string {
|
||||
if (src.startsWith("workspace://")) {
|
||||
const withoutPrefix = src.replace("workspace://", "");
|
||||
const fileId = withoutPrefix.split("#")[0];
|
||||
const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
|
||||
return `/api/proxy${apiPath}`;
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
function getWorkspaceMimeHint(src: string): string | undefined {
|
||||
const hashIndex = src.indexOf("#");
|
||||
if (hashIndex === -1) return undefined;
|
||||
return src.slice(hashIndex + 1) || undefined;
|
||||
}
|
||||
|
||||
function WorkspaceMedia({ value }: { value: string }) {
|
||||
const [imgFailed, setImgFailed] = useState(false);
|
||||
const resolvedUrl = resolveWorkspaceUrl(value);
|
||||
const mime = getWorkspaceMimeHint(value);
|
||||
|
||||
if (mime?.startsWith("video/") || imgFailed) {
|
||||
return (
|
||||
<video
|
||||
controls
|
||||
className="mt-2 h-auto max-w-full rounded-md border border-zinc-200"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={resolvedUrl} />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
if (mime?.startsWith("audio/")) {
|
||||
return <audio controls src={resolvedUrl} className="mt-2 w-full" />;
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolvedUrl}
|
||||
alt="Output media"
|
||||
className="mt-2 h-auto max-w-full rounded-md border border-zinc-200"
|
||||
loading="lazy"
|
||||
onError={() => setImgFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isWorkspaceRef(value: unknown): value is string {
|
||||
return typeof value === "string" && value.startsWith("workspace://");
|
||||
}
|
||||
|
||||
function renderOutputValue(value: unknown): React.ReactNode {
|
||||
if (isWorkspaceRef(value)) {
|
||||
return <WorkspaceMedia value={value} />;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const workspaceItems = value.filter(isWorkspaceRef);
|
||||
if (workspaceItems.length > 0) {
|
||||
return (
|
||||
<>
|
||||
{value.map((item, i) =>
|
||||
isWorkspaceRef(item) ? (
|
||||
<WorkspaceMedia key={i} value={item} />
|
||||
) : (
|
||||
<pre
|
||||
key={i}
|
||||
className="mt-1 whitespace-pre-wrap text-xs text-muted-foreground"
|
||||
>
|
||||
{formatMaybeJson(item)}
|
||||
</pre>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ViewAgentOutputTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const isStreaming =
|
||||
@@ -204,34 +187,33 @@ export function ViewAgentOutputTool({ part }: Props) {
|
||||
<ContentCardTitle className="text-xs">
|
||||
Inputs summary
|
||||
</ContentCardTitle>
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.execution.inputs_summary)}
|
||||
</pre>
|
||||
<div className="mt-2">
|
||||
<RenderOutputValue
|
||||
value={output.execution.inputs_summary}
|
||||
/>
|
||||
</div>
|
||||
</ContentCard>
|
||||
)}
|
||||
|
||||
{Object.entries(output.execution.outputs ?? {}).map(
|
||||
([key, items]) => {
|
||||
const mediaContent = renderOutputValue(items);
|
||||
return (
|
||||
<ContentCard key={key}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<ContentCardTitle className="text-xs">
|
||||
{key}
|
||||
</ContentCardTitle>
|
||||
<ContentBadge>
|
||||
{items.length} item
|
||||
{items.length === 1 ? "" : "s"}
|
||||
</ContentBadge>
|
||||
</div>
|
||||
{mediaContent || (
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(items.slice(0, 3))}
|
||||
</pre>
|
||||
)}
|
||||
</ContentCard>
|
||||
);
|
||||
},
|
||||
([key, items]) => (
|
||||
<ContentCard key={key}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<ContentCardTitle className="text-xs">
|
||||
{key}
|
||||
</ContentCardTitle>
|
||||
<ContentBadge>
|
||||
{items.length} item
|
||||
{items.length === 1 ? "" : "s"}
|
||||
</ContentBadge>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{items.slice(0, 3).map((item, i) => (
|
||||
<RenderOutputValue key={i} value={item} />
|
||||
))}
|
||||
</div>
|
||||
</ContentCard>
|
||||
),
|
||||
)}
|
||||
</ContentGrid>
|
||||
) : (
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Card } from "@/components/atoms/Card/Card";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircleIcon, QuestionIcon } from "@phosphor-icons/react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { CheckCircleIcon } from "@phosphor-icons/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface ClarifyingQuestion {
|
||||
question: string;
|
||||
@@ -114,23 +114,14 @@ export function ClarificationQuestionsWidget({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full max-w-3xl gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-green-500">
|
||||
<CheckCircleIcon className="h-4 w-4 text-white" weight="bold" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<Card className="p-4">
|
||||
<Text variant="h4" className="mb-1 text-slate-900">
|
||||
Answers submitted
|
||||
</Text>
|
||||
<Text variant="small" className="text-slate-600">
|
||||
Processing your responses...
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="w-full p-4">
|
||||
<Text variant="h4" className="mb-1 text-slate-900">
|
||||
Answers submitted
|
||||
</Text>
|
||||
<Text variant="small" className="text-slate-600">
|
||||
Processing your responses...
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -142,102 +133,92 @@ export function ClarificationQuestionsWidget({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full max-w-3xl gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-indigo-500">
|
||||
<QuestionIcon className="h-4 w-4 text-indigo-50" weight="bold" />
|
||||
</div>
|
||||
<Card className="w-full space-y-4 rounded-xl p-4">
|
||||
<div>
|
||||
<Text variant="h4" className="mb-1 text-slate-900">
|
||||
I need more information
|
||||
</Text>
|
||||
<Text variant="body" className="italic text-slate-600">
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<Card className="space-y-4 p-4">
|
||||
<div>
|
||||
<Text variant="h4" className="mb-1 text-slate-900">
|
||||
I need more information
|
||||
</Text>
|
||||
<Text variant="small" className="text-slate-600">
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{questions.map((q, index) => {
|
||||
const isAnswered = !!answers[q.keyword]?.trim();
|
||||
|
||||
<div className="space-y-3">
|
||||
{questions.map((q, index) => {
|
||||
const isAnswered = !!answers[q.keyword]?.trim();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${q.keyword}-${index}`}
|
||||
className={cn(
|
||||
"relative rounded-lg border p-3",
|
||||
isAnswered
|
||||
? "border-green-500 bg-green-50/50"
|
||||
: "border-slate-200 bg-white/50",
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-start gap-2">
|
||||
{isAnswered ? (
|
||||
<CheckCircleIcon
|
||||
size={16}
|
||||
className="mt-0.5 text-green-500"
|
||||
weight="bold"
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-0.5 flex h-4 w-4 items-center justify-center rounded-full border border-slate-300 bg-white text-xs text-slate-500">
|
||||
{index + 1}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<Text
|
||||
variant="small"
|
||||
className="mb-2 font-semibold text-slate-900"
|
||||
>
|
||||
{q.question}
|
||||
</Text>
|
||||
{q.example && (
|
||||
<Text
|
||||
variant="small"
|
||||
className="mb-2 italic text-slate-500"
|
||||
>
|
||||
Example: {q.example}
|
||||
</Text>
|
||||
)}
|
||||
<Input
|
||||
type="textarea"
|
||||
id={`clarification-${q.keyword}-${index}`}
|
||||
label={q.question}
|
||||
hideLabel
|
||||
placeholder="Your answer..."
|
||||
rows={2}
|
||||
value={answers[q.keyword] || ""}
|
||||
onChange={(e) =>
|
||||
handleAnswerChange(q.keyword, e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!allAnswered}
|
||||
className="flex-1"
|
||||
variant="primary"
|
||||
return (
|
||||
<div
|
||||
key={`${q.keyword}-${index}`}
|
||||
className={cn(
|
||||
"relative rounded-lg border p-3",
|
||||
isAnswered
|
||||
? "border-green-500 bg-green-50/50"
|
||||
: "border-slate-100 bg-white/50",
|
||||
)}
|
||||
>
|
||||
Submit Answers
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<div className="mb-2 flex items-start gap-2">
|
||||
{isAnswered ? (
|
||||
<CheckCircleIcon
|
||||
size={16}
|
||||
className="mt-0.5 text-green-500"
|
||||
weight="bold"
|
||||
/>
|
||||
) : (
|
||||
<div className="relative top-0 flex h-6 w-6 items-center justify-center rounded-full border border-purple-200 text-xs text-purple-400">
|
||||
{index + 1}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<Text
|
||||
variant="body"
|
||||
className="mb-2 font-semibold text-slate-900"
|
||||
>
|
||||
{q.question}
|
||||
</Text>
|
||||
{q.example && (
|
||||
<Text
|
||||
variant="body"
|
||||
className="mb-2 italic text-slate-500"
|
||||
>
|
||||
Example: {q.example}
|
||||
</Text>
|
||||
)}
|
||||
<Input
|
||||
type="textarea"
|
||||
id={`clarification-${q.keyword}-${index}`}
|
||||
label={q.question}
|
||||
hideLabel
|
||||
placeholder="Your answer..."
|
||||
rows={2}
|
||||
value={answers[q.keyword] || ""}
|
||||
onChange={(e) =>
|
||||
handleAnswerChange(q.keyword, e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!allAnswered}
|
||||
className="flex-1"
|
||||
variant="primary"
|
||||
>
|
||||
Submit Answers
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user