Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -521,12 +521,16 @@ export default function BoardStage(props: {
onErase,
eraseDisabled = false,
} = props;
const drawingActive = Boolean(phase && phase !== "idle");
const isGenerating =
phase === "queued" || phase === "sketch" || phase === "color";
const showPlaceholder = isGenerating && !imageUrl;
const drawingActive = showPlaceholder;
const resolvedActiveId = activeSwatchId ?? swatches[0]?.id;
const activeSwatch =
swatches.find((swatch) => swatch.id === resolvedActiveId) ?? swatches[0];
const resolvedPenBody = penBodyColor ?? activeSwatch?.penBody ?? "#F97316";
const resolvedPenTop = penTopColor ?? activeSwatch?.penTop ?? "#FB923C";
const stylusPhase = imageUrl || !isGenerating ? "idle" : (phase ?? "idle");
const canErase = Boolean(onErase) && !eraseDisabled;
const [eraseProgress, setEraseProgress] = React.useState(0);
const eraseTriggeredRef = React.useRef(false);
Expand Down Expand Up @@ -612,7 +616,12 @@ export default function BoardStage(props: {
<SurfaceTexture />
<div className="pointer-events-none absolute inset-0 doodle-board-grid opacity-20" />

<div className="absolute inset-0 flex items-center justify-center">
<div
className={cn(
"absolute inset-0 flex items-center justify-center",
showPlaceholder && "flex-col gap-3 text-center"
)}
>
<AnimatePresence mode="wait">
{imageUrl ? (
<motion.div
Expand All @@ -638,37 +647,31 @@ export default function BoardStage(props: {
/>
</div>
</motion.div>
) : drawingActive ? (
<motion.div
key="doodle-placeholder"
className="flex w-full max-w-[min(520px,90%)] flex-col items-center gap-3 rounded-[18px] border border-dashed border-neutral-300/80 bg-white/80 p-6 text-center shadow-[0_16px_40px_rgba(32,16,8,0.12)] backdrop-blur-sm"
initial={{ opacity: 0, y: 8, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.98 }}
transition={{ duration: 0.25, ease: "easeOut" }}
>
<div
className="h-2 w-40 rounded-full"
style={{
background: `linear-gradient(90deg, ${resolvedPenBody} 0%, rgba(255,255,255,0.6) 100%)`,
}}
aria-hidden
/>
<div className="text-sm font-semibold text-neutral-700">
Sketching your doodle…
</div>
<div className="text-xs text-neutral-500">
The pen is warming up with your color.
</div>
</motion.div>
) : null}
</AnimatePresence>
{showPlaceholder ? (
<>
<div
className="h-2 w-40 rounded-full"
style={{
background: `linear-gradient(90deg, ${resolvedPenBody} 0%, rgba(255,255,255,0.6) 100%)`,
}}
aria-hidden
/>
<div className="text-sm font-semibold text-neutral-700">
Sketching your doodle…
</div>
<div className="text-xs text-neutral-500">
The pen is warming up with your color.
</div>
</>
) : null}
</div>

<div className="pointer-events-none absolute inset-0">
{overlay}
<ToyStylusAnimator
phase={imageUrl ? "idle" : phase}
phase={stylusPhase}
reducedMotion={reducedMotion}
bodyColor={resolvedPenBody}
topColor={resolvedPenTop}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
setRtmConnected,
} from "@/store/reducers/global";
import { EMessageDataType, EMessageType, type IChatItem } from "@/types";
import type { IProgressEvent } from "@/manager/rtc/types";
import BoardStage, { DEFAULT_CRAYON_SWATCHES } from "./BoardStage";
import MagicCanvasBackground, {
type CreativeMode,
Expand Down Expand Up @@ -120,9 +121,9 @@ export default function ImmersiveShell() {
const colorDescriptor = lineColorDescriptor;
const colorLine =
colorName === "black"
? "Use black lines on white paper."
: `Use ${colorDescriptor} lines on white paper.`;
return `${raw}\n\nDoodle style notes: simple hand-drawn line art. Two colors only (${colorDescriptor} lines and white background). No shading, no gradients, no fills, no 3D or realistic rendering. No borders, frames, paper edges, or background props. Do not include pens, pencils, crayons, markers, hands, shadows, or signatures. ${colorLine}`;
? "Use black lines on a plain white background."
: `Use ${colorDescriptor} lines on a plain white background.`;
return `${raw}\n\nDoodle style notes: simple hand-drawn line art. Two colors only (${colorDescriptor} lines and a plain white background). No shading, no gradients, no fills, no 3D or realistic rendering. No borders, frames, paper texture, paper edges, desks, or background props. Never include pens, pencils, crayons, markers, hands, shadows, or signatures. Keep the subject isolated so the white background blends into the board. ${colorLine}`;
},
[lineColorDescriptor, lineColorName]
);
Expand Down Expand Up @@ -150,6 +151,12 @@ export default function ImmersiveShell() {
(text: IChatItem) => dispatch(addChatItem(text)),
[dispatch]
);
const [progressPhase, setProgressPhase] = React.useState<
IProgressEvent["phase"] | null
>(null);
const onProgressChanged = React.useCallback((progress: IProgressEvent) => {
setProgressPhase(progress.phase);
}, []);

const latestImage = React.useMemo(() => getLatestImage(chatItems), [chatItems]);
const visibleImage = React.useMemo(() => {
Expand All @@ -165,24 +172,24 @@ export default function ImmersiveShell() {
),
[chatItems]
);
const lastAssistantTime = React.useMemo(
() =>
getLastTime(
chatItems,
(i) => i.type === EMessageType.AGENT && i.data_type === EMessageDataType.TEXT
),
[chatItems]
);
const lastImageTime = latestImage?.time ?? 0;
const hasRequest = lastUserTime > 0;
const generationStarted =
hasRequest && lastAssistantTime >= lastUserTime;
const isGenerating =
generationStarted && lastImageTime < lastAssistantTime;
const imageArrived = hasRequest && lastImageTime >= lastUserTime;

React.useEffect(() => {
if (lastUserTime <= 0) return;
setProgressPhase(null);
}, [lastUserTime]);

const [phase, setPhase] = React.useState<DoodlePhase>("idle");
React.useEffect(() => {
if (isGenerating) {
if (imageArrived) {
setPhase("complete");
const t = window.setTimeout(() => setPhase("idle"), 1700);
return () => window.clearTimeout(t);
}

if (progressPhase === "queued") {
setPhase("queued");
const t1 = window.setTimeout(() => setPhase("sketch"), 450);
const t2 = window.setTimeout(() => setPhase("color"), 1550);
Expand All @@ -192,15 +199,21 @@ export default function ImmersiveShell() {
};
}

if (hasRequest && lastImageTime >= lastAssistantTime) {
if (progressPhase === "drawing") {
setPhase("sketch");
const t = window.setTimeout(() => setPhase("color"), 1100);
return () => window.clearTimeout(t);
}

if (progressPhase === "final") {
setPhase("complete");
const t = window.setTimeout(() => setPhase("idle"), 1700);
return () => window.clearTimeout(t);
}

setPhase("idle");
return;
}, [hasRequest, isGenerating, lastImageTime, lastAssistantTime]);
}, [imageArrived, progressPhase]);

const canConnect = channel.trim().length > 0 && userId > 0;
const controlsEnabled = roomConnected && agentConnected && rtmConnected;
Expand Down Expand Up @@ -303,6 +316,8 @@ export default function ImmersiveShell() {
}
rtc.off("textChanged", onTextChanged);
rtc.on("textChanged", onTextChanged);
rtc.off("progressChanged", onProgressChanged);
rtc.on("progressChanged", onProgressChanged);
await rtc.createMicrophoneAudioTrack();
const track: IMicrophoneAudioTrack | undefined = rtc.localTracks?.audioTrack;
setMicTrack(track);
Expand Down Expand Up @@ -366,11 +381,13 @@ export default function ImmersiveShell() {
try {
await rtmRef.current?.destroy?.();
rtcRef.current?.off?.("textChanged", onTextChanged);
rtcRef.current?.off?.("progressChanged", onProgressChanged);
await rtcRef.current?.destroy?.();
} finally {
dispatch(setRtmConnected(false));
dispatch(setRoomConnected(false));
dispatch(setAgentConnected(false));
setProgressPhase(null);
setMicMediaTrack(undefined);
setMicTrack(undefined);
toast.message("Disconnected.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type ITextItem,
} from "@/types";
import { AGEventEmitter } from "../events";
import type { IUserTracks, RtcEvents } from "./types";
import type { IProgressEvent, IUserTracks, RtcEvents } from "./types";

const TIMEOUT_MS = 5000; // Timeout for incomplete messages

Expand Down Expand Up @@ -267,6 +267,18 @@ export class RtcManager extends AGEventEmitter<RtcEvents> {
text: data.text,
};
} else if (type === "progress") {
if (
data?.phase === "queued" ||
data?.phase === "drawing" ||
data?.phase === "final"
) {
const progressEvent: IProgressEvent = {
phase: data.phase,
pct: typeof data?.pct === "number" ? data.pct : undefined,
time: text_ts ?? Date.now(),
};
this.emit("progressChanged", progressEvent);
}
return;
} else if (type === "action") {
const { action, data: actionData } = data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import {
} from "agora-rtc-sdk-ng";
import { type IChatItem, ITextItem } from "@/types";

export type ImageProgressPhase = "queued" | "drawing" | "final";

export interface IProgressEvent {
phase: ImageProgressPhase;
pct?: number;
time: number;
}

export interface IRtcUser {
userId: UID;
videoTrack?: IRemoteVideoTrack;
Expand All @@ -23,6 +31,7 @@ export interface RtcEvents {
localTracksChanged: (tracks: IUserTracks) => void;
networkQuality: (quality: NetworkQuality) => void;
textChanged: (text: IChatItem) => void;
progressChanged: (progress: IProgressEvent) => void;
}

export interface IUserTracks {
Expand Down
2 changes: 1 addition & 1 deletion ai_agents/agents/examples/doodler/tenapp/property.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"api_key": "${env:OPENAI_API_KEY}",
"model": "${env:OPENAI_MODEL|gpt-4o-mini}",
"max_tokens": 512,
"prompt": "You are a friendly AI art assistant for kids! When children describe what they want to draw, help them create amazing doodles.\n\nGuidelines:\n- Keep responses short, fun, and encouraging\n- When they describe an image idea, use the generate_image tool immediately\n- Always request simple hand-drawn doodle line art on white paper\n- Use only two colors: the line color and the white background (no shading, gradients, or fills)\n- Default to black lines unless the child or UI specifies another line color\n- Avoid 3D, animation, realism, or painterly rendering\n- Always say \"drawing\" instead of \"generating\"\n- After drawing, celebrate their creativity!\n- If the image can't be created, gently suggest something similar\n\nExample:\nKid: \"I want a purple dragon!\"\nYou: \"Ooh, a purple dragon! Let me draw that for you!\" [calls generate_image with a simple doodle prompt: purple lines on white paper, two colors only]",
"prompt": "You are a friendly AI art assistant for kids! When children describe what they want to draw, help them create amazing doodles.\n\nGuidelines:\n- Keep responses short, fun, and encouraging\n- When they describe an image idea, use the generate_image tool immediately\n- Always request simple hand-drawn doodle line art on a plain white background (no paper texture or edges)\n- Use only two colors: the line color and the white background (no shading, gradients, or fills)\n- Default to black lines unless the child or UI specifies another line color\n- Avoid 3D, animation, realism, or painterly rendering\n- Never include pens, pencils, markers, hands, desks, or background props\n- Always say \"drawing\" instead of \"generating\"\n- After drawing, celebrate their creativity!\n- If the image can't be created, gently suggest something similar\n\nExample:\nKid: \"I want a purple dragon!\"\nYou: \"Ooh, a purple dragon! Let me draw that for you!\" [calls generate_image with a simple doodle prompt: purple lines on a plain white background, two colors only]",
"greeting": "Hey doodle buddy! Ready to draw together? Tell me what you want to doodle!",
"max_memory_length": 10
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,11 @@ async def run_tool(
),
)
doodle_modifier = (
" in playful crayon doodle style on white paper, hand-drawn, bold uneven outlines, "
" in playful crayon doodle style, hand-drawn, bold uneven outlines, "
"simple shapes, flat colors, limited palette, minimal detail, no gradients, no 3D, "
"no realistic lighting, no photo realism, kid-friendly and cheerful"
"no realistic lighting, no photo realism, kid-friendly and cheerful, "
"plain white background only (no paper texture or borders), "
"no pens, pencils, markers, hands, desks, or background props"
)
prompt = f"{prompt.strip()}{doodle_modifier}"

Expand Down
Loading