Skip to content
Open
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
352 changes: 300 additions & 52 deletions templates/io-assist-react-editable/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions templates/io-assist-react-editable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
},
"dependencies": {
"@fontsource-variable/inter": "^5.0.0",
"@interopio/ai-web": "^1.1.2",
"@interopio/ai-web": "^1.2.2",
"@interopio/browser": "^4.5.0",
"@interopio/desktop": "^6.21.0",
"@interopio/working-context": "^1.1.1",
"@interopio/working-context": "^1.1.2",
"@interopio/workspaces-api": "^4.5.0",
"prism-react-renderer": "^2.4.1",
"react": "^19.2.6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ function buildWebConfig(staticConfig: IoAssistStaticConfig, dynamicConfig: IoAss
config.context = staticConfig.workingContext;
}

if (staticConfig.aiWebConfig.codeMode) {
config.codeMode = staticConfig.aiWebConfig.codeMode;
}

return config;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Chat: React.FC = () => {
const showMessageArea = hasMessages || isLoadingThread;
const showWelcomeMessage = !showMessageArea;
const showFavoritePrompts = showWelcomeMessage && favoritePromptNames.length > 0;
const agentName = dynamicConfig.user.name ?? dynamicConfig.user.id;
const agentName = dynamicConfig.user.name;

// Auto-close sidebar when the viewport shrinks below md (parity with ng's
// ResponsiveUIService effect). Intentionally depends only on `isMobile` so
Expand Down Expand Up @@ -65,7 +65,7 @@ export const Chat: React.FC = () => {
const innerColumnClassNames = ["flex flex-1 min-h-0 min-w-0 flex-col items-center w-full custom-scrollbar-snap-right", showMessageArea ? "gap-0" : "gap-6"].join(" ");

return (
<div data-testid="io-assist-root" className="flex h-full w-full overflow-hidden bg-app-background">
<div data-testid="io-assist-root" className="io-assist-root flex h-full w-full overflow-hidden bg-app-background">
<div data-testid="chat" className="flex flex-1 min-h-0 overflow-hidden">
{isThreadHistoryVisible && isMobile && (
<div role="presentation" className="fixed inset-0 z-20 bg-black/45" onClick={closeSidebar} onKeyDown={(e) => e.key === "Escape" && closeSidebar()} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from "react";
import { UI_STRINGS } from "../../constants/uiStrings";

type Props = {
agentName: string;
agentName?: string;
};

export const WelcomeHeading: React.FC<Props> = ({ agentName }) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const Header: React.FC = () => {
<button
type="button"
data-testid="working-context-button"
className="flex justify-center items-center px-2 py-[3.5px] border-[1px] rounded-[8px] text-text-default cursor-pointer hover:text-text-states-hover font-weight-400 text-[10px] leading-[13px]"
className="flex justify-center items-center px-2 py-[3.5px] border-[1px] rounded-[8px] text-text-default cursor-pointer hover:text-text-states-hover font-weight-350 text-[10px] leading-[13px]"
onClick={handleWorkingContextOpen}
>
{UI_STRINGS.WORKING_CONTEXT_PANEL_COMPONENT.VIEW_WORKING_CONTEXT_BUTTON}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const InputArea: React.FC<Props> = ({ onSend, onAbort, className }) => {
<div className="input-area__container relative w-full">
<div className="input-area__content accent-gradient-border relative rounded-2xl bg-app-background">
<div className="flex flex-col gap-4 p-4">
<div className="flex gap-3 items-start px-2">
<div className="flex gap-3 items-start ps-[1px] pt-1">
<textarea
ref={textareaRef}
data-testid="chat-input-field"
Expand Down Expand Up @@ -203,20 +203,19 @@ const ActionBar: React.FC = () => {

const [hovered, setHovered] = useState<"prompt" | "tool" | null>(null);

// Match ng: size depends only on whether the panel is displayed (XS when open, S otherwise) —
// it must NOT shrink on hover. The icon variant, however, swaps to the filled glyph on hover.
const promptDisplayed = activePanelContent === PANEL_CONTENT.PROMPTS;
const toolDisplayed = activePanelContent === PANEL_CONTENT.TOOLS;
const promptFilled = promptDisplayed || hovered === "prompt";
const toolFilled = toolDisplayed || hovered === "tool";

return (
<div className="flex flex-1 gap-0.5 items-center">
<div className="flex flex-1 gap-1 items-center">
<IconButton
size={promptDisplayed ? 12 : 16}
containerSize={promptDisplayed ? 24 : 32}
size={12}
containerSize={24}
testId="prompt-panel-button"
className="hover:bg-app-icon-background-hover"
shape="rounded-rectangle"
aria-pressed={promptDisplayed}
onClick={() => setActivePanelContent(PANEL_CONTENT.PROMPTS)}
onMouseEnter={() => setHovered("prompt")}
onMouseLeave={() => setHovered(null)}
Expand All @@ -226,10 +225,11 @@ const ActionBar: React.FC = () => {
{promptFilled ? <PromptPanelFilledIcon /> : <PromptPanelIcon />}
</IconButton>
<IconButton
size={toolDisplayed ? 12 : 16}
containerSize={toolDisplayed ? 24 : 32}
size={12}
containerSize={24}
testId="tool-panel-button"
className="hover:bg-app-icon-background-hover"
shape="rounded-rectangle"
aria-pressed={toolDisplayed}
onClick={() => setActivePanelContent(PANEL_CONTENT.TOOLS)}
onMouseEnter={() => setHovered("tool")}
onMouseLeave={() => setHovered(null)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Props = {

export const AssistantMessage: React.FC<Props> = ({ message, showFooter = false, footerCopyText = "", onReload, isGenerating = false }) => {
return (
<div data-testid="assistant-message" className="flex flex-col flex-1 min-w-0 max-w-full self-stretch text-text-states-active font-weight-weight-400 leading-[19px] safe-word-break">
<div data-testid="assistant-message" className="flex flex-col flex-1 min-w-0 max-w-full self-stretch text-text-states-active font-weight-350 leading-[19px] safe-word-break">
{message.content && (
<div data-testid="assistant-message-text">
<MdFormatter content={message.content} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const components: Components = {
const isInline = !className && typeof children === "string" && !children.includes("\n");
if (isInline) {
return (
<code className="bg-app-background-secondary px-1.5 py-0.5 rounded text-sm font-mono text-text-states-active" {...props}>
<code className="bg-app-background-secondary px-1.5 py-0.5 rounded text-sm font-mono text-[var(--app-blue)]" {...props}>
{children}
</code>
);
Expand All @@ -59,7 +59,7 @@ const components: Components = {
<a
href={href}
title={title ?? ""}
className="text-[14px] font-semibold leading-5 text-[var(--app-blue)] underline [text-decoration-skip-ink:none] hover:opacity-[0.85]"
className="text-[14px] font-weight-550 leading-5 text-[var(--app-blue)] underline [text-decoration-skip-ink:none] hover:opacity-[0.85]"
target="_blank"
rel="noopener noreferrer"
>
Expand All @@ -74,7 +74,7 @@ const components: Components = {
return <thead className="text-left">{children}</thead>;
},
th({ children }) {
return <th className="py-3 pr-3 pl-0 last:pr-0 font-medium text-[14px] leading-5 text-text-states-active border-b-2 border-panel-border">{children}</th>;
return <th className="py-3 pr-3 pl-0 last:pr-0 font-weight-550 text-[14px] leading-5 text-text-states-active border-b-2 border-panel-border">{children}</th>;
},
td({ children }) {
return <td className="py-3 pr-3 pl-0 last:pr-0 text-[14px] leading-5 text-text-default">{children}</td>;
Expand All @@ -87,43 +87,47 @@ const components: Components = {
);
},
ul({ children }) {
return <ul className="list-disc pl-5 mt-4 mb-2 text-[14px] font-normal leading-5 text-text-default">{children}</ul>;
return <ul className="text-[14px] font-weight-350 leading-5 text-text-default">{children}</ul>;
},
ol({ children }) {
return <ol className="list-decimal pl-5 mt-4 mb-2 text-[14px] font-normal leading-5 text-text-default">{children}</ol>;
return <ol className="text-[14px] font-weight-350 leading-5 text-text-default">{children}</ol>;
},
li({ children }) {
return <li className="py-1">{children}</li>;
return <li>{children}</li>;
},
blockquote({ children }) {
return <blockquote className="p-4 my-4 rounded-2xl text-text-default flex flex-col gap-1 relative border border-app-accent-color-1">{children}</blockquote>;
return (
<blockquote className="relative flex flex-col gap-1 py-4 px-6 my-2 rounded-[4px] text-text-default bg-[#2e3033] before:content-[''] before:absolute before:left-0 before:inset-y-0 before:w-1.5 before:rounded-l-[4px] before:bg-[linear-gradient(180deg,var(--color-app-accent-color-1),var(--app-blue))]">
{children}
</blockquote>
);
},
hr() {
return <hr className="my-4 border-0 border-t border-panel-border" />;
},
p({ children }) {
return <p className="text-[14px] font-normal leading-5 text-text-default my-2 first:mt-0 last:mb-0">{children}</p>;
return <p className="text-[14px] font-weight-350 leading-5 text-text-default my-2 first:mt-0 last:mb-0">{children}</p>;
},
strong({ children }) {
return <strong className="font-semibold">{children}</strong>;
return <strong className="font-weight-550">{children}</strong>;
},
h1({ children }) {
return <h1 className="font-default font-medium text-text-states-active py-0 mt-5 text-[20px] leading-[28px]">{children}</h1>;
return <h1 className="font-default font-weight-550 text-text-states-active py-0 mt-5 text-[20px] leading-[28px]">{children}</h1>;
},
h2({ children }) {
return <h2 className="font-default font-medium text-text-states-active py-0 mt-5 text-[18px] leading-[26px]">{children}</h2>;
return <h2 className="font-default font-weight-550 text-text-states-active py-0 mt-5 text-[18px] leading-[26px]">{children}</h2>;
},
h3({ children }) {
return <h3 className="font-default font-medium text-text-states-active py-0 mt-5 text-[16px] leading-[24px]">{children}</h3>;
return <h3 className="font-default font-weight-550 text-text-states-active py-0 mt-5 text-[16px] leading-[24px]">{children}</h3>;
},
h4({ children }) {
return <h4 className="font-default font-medium text-text-states-active py-0 mt-5 text-[15px] leading-[22px]">{children}</h4>;
return <h4 className="font-default font-weight-550 text-text-states-active py-0 mt-5 text-[15px] leading-[22px]">{children}</h4>;
},
h5({ children }) {
return <h5 className="font-default font-medium text-text-states-active py-0 mt-5 text-[14px] leading-5">{children}</h5>;
return <h5 className="font-default font-weight-550 text-text-states-active py-0 mt-5 text-[14px] leading-5">{children}</h5>;
},
h6({ children }) {
return <h6 className="font-default font-medium text-text-states-active py-0 mt-5 text-[13px] leading-[18px]">{children}</h6>;
return <h6 className="font-default font-weight-550 text-text-states-active py-0 mt-5 text-[13px] leading-[18px]">{children}</h6>;
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,7 @@ export const MessageArea: React.FC<Props> = ({ onReloadResponse, className }) =>
<div className="flex flex-1 justify-center items-center p-4">
<div className="flex gap-1">
{[0, 1, 2].map((i) => (
<div
key={i}
className="size-1.5 rounded-full bg-app-accent-color-1"
style={{
animationDelay: `${i * 0.15}s`,
animation: `rise 0.6s cubic-bezier(0.16,1,0.3,1) ${i * 0.15}s infinite alternate`,
}}
/>
<div key={i} className="loading-dot size-1.5 rounded-full bg-app-accent-color-1" style={{ animationDelay: `${i * 0.15}s` }} />
))}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const ToolMessage: React.FC<Props> = ({ tool, parentToolTraceId, onToggle
const resultMd = useMemo(() => resultToJSONMarkdown(tool.result), [tool.result]);

return (
<div data-testid="tool-message" className="flex flex-col justify-start py-2 px-[3px] text-[13px] font-weight-weight-600 leading-[100%] text-text-states-active">
<div data-testid="tool-message" className="flex flex-col justify-start py-2 px-[3px] text-[13px] font-weight-550 leading-[100%] text-text-states-active">
<div className="flex items-center gap-2 px-1">
<span
data-testid="tool-item-dot"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ export const UserMessage: React.FC<Props> = ({ message, isLast = false, isGenera
</div>

<div className="flex min-w-0">
<div className="p-4 list-none text-[14px] leading-[18px] font-weight-weight-400 text-user-message-text bg-user-message-background rounded-2xl max-w-[620px] min-w-0 whitespace-pre-wrap break-words">
<div className="p-4 list-none text-[14px] leading-[18px] font-weight-350 text-user-message-text bg-user-message-background rounded-2xl max-w-[620px] min-w-0 whitespace-pre-wrap break-words">
{message.content ?? ""}
</div>
</div>
</div>

{showError && <div className="flex justify-end py-1 text-text-default text-[13px] font-weight-weight-400 leading-[100%]">{error}</div>}
{showError && <div className="flex justify-end py-1 text-text-default text-[13px] font-weight-350 leading-[100%]">{error}</div>}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,52 @@ import React, { useState } from "react";

import { useIoAssistStore } from "../../context/IoAssistContext";
import { useIoConnectApi } from "../../hooks/useIoConnectApi";
import type { Prompt } from "../../types";
import type { IconResource, Prompt } from "../../types";
import { Icon } from "../shared/Icon";
import { PromptDotIcon, StarIcon, StarFilledIcon } from "../shared/icons";
import { PromptPanelIcon, StarIcon, StarFilledIcon } from "../shared/icons";

type Props = {
prompt: Prompt;
isDisplayedInFavoriteList: boolean;
};

const getIconMaskUrl = (iconResource: IconResource): string => {
if (iconResource.type === "svg") {
return `url(${JSON.stringify(`data:image/svg+xml,${encodeURIComponent(iconResource.data)}`)})`;
}

return `url(${JSON.stringify(iconResource.data)})`;
};

const PromptItemIcon: React.FC<{ iconResource?: IconResource }> = ({ iconResource }) => {
if (!iconResource) {
return (
<Icon size={12}>
<PromptPanelIcon />
</Icon>
);
}

const maskImage = getIconMaskUrl(iconResource);

return (
<span
aria-hidden="true"
className="block size-3 bg-current"
style={{
WebkitMaskImage: maskImage,
maskImage,
WebkitMaskPosition: "center",
maskPosition: "center",
WebkitMaskRepeat: "no-repeat",
maskRepeat: "no-repeat",
WebkitMaskSize: "contain",
maskSize: "contain",
}}
/>
);
};

export const PromptListItem: React.FC<Props> = ({ prompt, isDisplayedInFavoriteList }) => {
const [isHovered, setIsHovered] = useState(false);
const favoritePromptNames = useIoAssistStore((s) => s.favoritePromptNames);
Expand Down Expand Up @@ -56,10 +93,8 @@ export const PromptListItem: React.FC<Props> = ({ prompt, isDisplayedInFavoriteL
}}
title={prompt.description || undefined}
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-full text-text-default">
<Icon size={16}>
<PromptDotIcon />
</Icon>
<span data-testid="prompt-icon" className={["flex size-6 shrink-0 items-center justify-center", isHovered ? "text-text-states-hover" : "text-text-default"].join(" ")}>
<PromptItemIcon iconResource={prompt.iconResource} />
</span>

<span className={["flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-xs", isHovered ? "text-text-states-hover" : "text-text-default"].join(" ")}>{prompt.name}</span>
Expand All @@ -71,7 +106,7 @@ export const PromptListItem: React.FC<Props> = ({ prompt, isDisplayedInFavoriteL
aria-label={isFavorite ? "Remove from favorites" : "Add to favorites"}
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
className={[
"p-2 ps-1 shrink-0 inline-flex items-center justify-center cursor-pointer bg-transparent border-0",
"size-6 p-2 ps-1 shrink-0 inline-flex items-center justify-center cursor-pointer bg-transparent border-0",
isFavorite ? "text-yellow-400" : isHovered ? "text-text-default hover:text-yellow-400" : "text-text-states-disabled",
].join(" ")}
onClick={handleToggleFavorite}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,37 @@ import React from "react";
import { Icon } from "./Icon";

type IconButtonVariant = "default" | "danger" | "plain";
type IconButtonShape = "circle" | "rounded-rectangle";

type IconButtonProps = {
size?: number;
containerSize?: number;
testId?: string;
variant?: IconButtonVariant;
shape?: IconButtonShape;
children: React.ReactNode;
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children">;

const ICON_BUTTON_HOVER_BY_VARIANT: Record<IconButtonVariant, string> = {
default: "hover:text-text-states-hover",
default: "hover:text-icon-hover",
danger: "hover:text-app-danger",
// No hover color change — for buttons that animate/keep their color on hover
// (e.g. ng's panel/modal close icon keeps `--app-text-black-white`).
plain: "",
};

export const IconButton: React.FC<IconButtonProps> = ({ size = 16, containerSize, className, testId, style, type = "button", variant = "default", children, ...rest }) => (
const ICON_BUTTON_SHAPE_CLASS: Record<IconButtonShape, string> = {
circle: "rounded-full",
"rounded-rectangle": "rounded-lg",
};

export const IconButton: React.FC<IconButtonProps> = ({ size = 16, containerSize, className, testId, style, type = "button", variant = "default", shape = "circle", children, ...rest }) => (
<button
type={type}
data-testid={testId}
className={[
"flex items-center justify-center cursor-pointer transition-colors rounded-full text-text-default disabled:opacity-50 disabled:cursor-not-allowed",
"flex items-center justify-center cursor-pointer transition-colors text-icon-default disabled:text-icon-disabled disabled:cursor-not-allowed",
ICON_BUTTON_SHAPE_CLASS[shape],
ICON_BUTTON_HOVER_BY_VARIANT[variant],
className ?? "",
].join(" ")}
Expand Down
Loading