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
26 changes: 22 additions & 4 deletions src/addons/addons/02agent/components/AIAssistantIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import * as React from "react";
import Icon02Agent from "assets/icon-02agent.svg";
// @ts-expect-error
import Icon02Agent from "../assets/icon-02agent.svg";

export const AIAssistantIcon = () => (
<img src={Icon02Agent} aria-hidden="true" alt="" width={18} height={18} style={{ width: 18, height: 18 }} />
);
// export const AIAssistantIcon = () => (
// <img src={Icon02Agent} aria-hidden="true" alt="" width={18} height={18} style={{ width: 18, height: 18 }} />
// );
export function AIAssistantIcon() {
return (
<img
src={Icon02Agent}
aria-hidden="true"
alt=""
draggable={false}
width={18}
height={18}
style={{
width: 18,
height: 18,
userSelect: "none"
}}
/>
);
}
91 changes: 91 additions & 0 deletions src/addons/addons/02agent/components/Launcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from "react";
import Draggable from "react-draggable";
import styles from "../styles.less";
import Tooltip from "../shims/components/Tooltip";
import { useStoredState } from "../hooks/useStoredState";
import { AIAssistantIcon } from "./AIAssistantIcon";

interface LauncherProps {
themeMode: "dark" | "light";
onToggle: () => void;
}

const clampRectToViewport = (rect: DOMRect) => ({
x: Math.max(0, Math.min(rect.left, window.innerWidth - rect.width)),
y: Math.max(0, Math.min(rect.top, window.innerHeight - rect.height)),
});

const Launcher: React.FC<LauncherProps> = ({ themeMode, onToggle }) => {
const [launcherPosition, setLauncherPosition] = useStoredState("02AGENT_LAUNCHER_POSITION", { x: 0, y: 0 });
const containerRef = React.useRef<HTMLElement | null>(null);
const launcherDraggedRef = React.useRef(false);

React.useEffect(() => {
const keepInViewport = () => {
if (!containerRef.current) return;
const next = clampRectToViewport(containerRef.current.getBoundingClientRect());
setLauncherPosition((prev) => {
if (prev.x === next.x && prev.y === next.y) return prev;
return next;
});
};

keepInViewport();
window.addEventListener("resize", keepInViewport);
return () => window.removeEventListener("resize", keepInViewport);
}, []);

return (
<div
style={{
margin: 0,
padding: 0,
position: "fixed",
left: 0,
top: 0,
width: "100vw",
height: "100vh",
zIndex: 99999999,
}}
className="tw-02agent-launcher-container"
>
{/** @ts-expect-error */}
<Draggable
handle=".tw-02agent-launcher-handle"
cancel="input, textarea, select, option, [contenteditable=true]"
position={launcherPosition}
bounds=".tw-02agent-launcher-container"
onStart={() => {
launcherDraggedRef.current = false;
}}
onDrag={() => {
launcherDraggedRef.current = true;
}}
onStop={(_, data) => {
setLauncherPosition({ x: data.x, y: data.y });
window.setTimeout(() => {
launcherDraggedRef.current = false;
}, 0);
}}
>
<section className={styles.aiAssistantRoot} ref={containerRef}>
<Tooltip
className={`tw-02agent-launcher-handle ${styles.icon} ${themeMode === "dark" ? styles.iconDark : styles.iconLight}`}
icon={
<>
<AIAssistantIcon />
<span>02Agent</span>
</>
}
onClick={() => {
if (!launcherDraggedRef.current) onToggle();
}}
tipText={"02Agent"}
/>
</section>
</Draggable>
</div>
);
};

export default Launcher;
56 changes: 12 additions & 44 deletions src/addons/addons/02agent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import * as React from "react";
import ReactDOM from "react-dom";
import Draggable from "react-draggable";
import styles from "./styles.less";
import themeStyles from "./ui/Theme.module.less";
import shell from "./ui/Shell.module.less";
import Tooltip from "./shims/components/Tooltip";
import ExpansionBox, { ExpansionRect } from "./shims/components/ExpansionBox.tsx";
import { useStoredState } from "./hooks/useStoredState";
import { registerContextMenu } from "./contextMenu";
import { AIAssistantIcon } from "./components/AIAssistantIcon";
import Launcher from "./components/Launcher";
import { HistoryPanel } from "./components/HistoryPanel";
import { SettingsModal } from "./components/SettingsModal";
import { ChatArea } from "./components/ChatArea";
Expand Down Expand Up @@ -40,12 +37,10 @@ type AgentProps = PluginContext & { editorThemeMode?: ThemeMode };
const Agent: React.FC<AgentProps> = ({ vm, workspace, editorThemeMode = "light" }) => {
console.log(`[02Agent] Rendering\n vm:`, vm)
const [visible, setVisible] = React.useState(false);
const [launcherPosition, setLauncherPosition] = useStoredState("02AGENT_LAUNCHER_POSITION", { x: 0, y: 0 });

const [isAgentMenuOpen, setIsAgentMenuOpen] = React.useState(false);
const [isComposerExpanded, setIsComposerExpanded] = React.useState(false);
const [themeMode, setThemeMode] = React.useState<ThemeMode>(editorThemeMode);
const containerRef = React.useRef(null);
const launcherDraggedRef = React.useRef(false);
const agentMenuRef = React.useRef<HTMLDivElement | null>(null);
const [enableReasoning, setEnableReasoning] = useStoredState<boolean>("02AGENT_ENABLE_REASONING", false);

Expand Down Expand Up @@ -130,22 +125,22 @@ const Agent: React.FC<AgentProps> = ({ vm, workspace, editorThemeMode = "light"
};
}, []);

const handleShow = React.useCallback(() => {
const handleToggle = React.useCallback(() => {
setContainerInfo({
...containerInfoRef.current,
...getContainerPosition(),
});
setVisible(true);
}, [getContainerPosition, setContainerInfo]);
setVisible(!visible);
}, [getContainerPosition, setContainerInfo, visible]);

const handleClose = () => {
const handleClose = React.useCallback(() => {
setVisible(false);
};

}, []);
const handleMinimize = () => {
setVisible(false);
};


const handleRestoreToUserMessage = React.useCallback(
async (messageId: string, message: { content: string; attachments?: Attachment[] }) => {
const result = rollbackToMessage(messageId, message.content, message.attachments || []);
Expand Down Expand Up @@ -273,41 +268,16 @@ const Agent: React.FC<AgentProps> = ({ vm, workspace, editorThemeMode = "light"
contextMenuRegistration.dispose();
window.removeEventListener("02agent-add-context", handleAddContext);
};
}, [vm, workspace, handleShow, setAttachments]);
}, [vm, workspace, handleToggle, setAttachments]);

if (!pluginsWrapper) {
console.warn("[02Agent] No portal target found (.plugins-wrapper or #gandi-plugins-wrapper)");
}

return ReactDOM.createPortal(
<>
<Draggable
handle=".tw-02agent-launcher-handle"
cancel="input, textarea, select, option, [contenteditable=true]"
position={launcherPosition}
onStart={() => {
launcherDraggedRef.current = false;
}}
onDrag={() => {
launcherDraggedRef.current = true;
}}
onStop={(_, data) => {
setLauncherPosition({ x: data.x, y: data.y });
window.setTimeout(() => {
launcherDraggedRef.current = false;
}, 0);
}}
>
<section className={styles.aiAssistantRoot} ref={containerRef}>
<Tooltip
className={`tw-02agent-launcher-handle ${styles.icon} ${themeMode === "dark" ? styles.iconDark : styles.iconLight}`}
icon={<><AIAssistantIcon /><span>02Agent</span></>}
onClick={() => {
if (!launcherDraggedRef.current) handleShow();
}}
tipText={"02Agent"}
/>
{visible &&
<Launcher themeMode={themeMode} onToggle={handleToggle} />
{visible &&
ReactDOM.createPortal(
<ExpansionBox
id="02agent"
Expand All @@ -322,7 +292,7 @@ const Agent: React.FC<AgentProps> = ({ vm, workspace, editorThemeMode = "light"
borderRadius={8}
>
<div
className={`${styles.container} ${shell.appShell} ${themeStyles.themeRoot} ${
className={`${shell.container} ${shell.appShell} ${themeStyles.themeRoot} ${
themeMode === "dark" ? themeStyles.themeDark : themeStyles.themeLight
}`}
>
Expand Down Expand Up @@ -495,8 +465,6 @@ const Agent: React.FC<AgentProps> = ({ vm, workspace, editorThemeMode = "light"
</ExpansionBox>,
document.body,
)}
</section>
</Draggable>
{/*<ConverterDebugger vm={vm} workspace={workspace} />*/}
</>,
pluginsWrapper,
Expand Down
26 changes: 20 additions & 6 deletions src/addons/addons/02agent/shims/components/ExpansionBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,19 @@ const ExpansionBox = ({
x: Math.max(containerInfo.translateX || 0, 0),
y: Math.max(containerInfo.translateY || 0, 0)
});
const handleOnMinimize = React.useCallback(async ()=>{
if(!windowRef.current) return;
windowRef.current.style.scale = "0";
windowRef.current.style.opacity = "0";
await new Promise(resolve => setTimeout(resolve, 200));
onMinimize?.();
},[onMinimize])
React.useEffect(()=>{
if(!windowRef.current) return;
windowRef.current.style.scale = "1";
windowRef.current.style.opacity = "1";

},[])
React.useEffect(() => {
setPosition({
x: Math.max(containerInfo.translateX || 0, 0),
Expand Down Expand Up @@ -139,7 +151,10 @@ const ExpansionBox = ({
display: "flex",
flexDirection: "column",
background: isDark ? "#152223" : "#f4fbfa",
boxShadow: isDark ? "0 18px 46px rgba(0, 0, 0, 0.38)" : "0 18px 46px rgba(16, 72, 68, 0.24)"
boxShadow: isDark ? "0 18px 46px rgba(0, 0, 0, 0.38)" : "0 18px 46px rgba(16, 72, 68, 0.24)",
scale: 0,
opacity: 0,
transition: "scale 0.2s ease-in-out, opacity 0.2s ease-in-out"
}}
>
<div
Expand All @@ -157,17 +172,17 @@ const ExpansionBox = ({
userSelect: "none"
}}
>
{onMinimize ? (

<button
type="button"
onClick={onMinimize}
onClick={handleOnMinimize}
title="最小化到后台"
style={{ position: "absolute", right: 34, background: "transparent", border: 0, color: "inherit" }}
>
</button>
) : null}
{onClose ? (


<button
type="button"
onClick={onClose}
Expand All @@ -176,7 +191,6 @@ const ExpansionBox = ({
>
×
</button>
) : null}
<strong>{title}</strong>
</div>
{children}
Expand Down
1 change: 1 addition & 0 deletions src/addons/addons/02agent/styles.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.aiAssistantRoot {
pointer-events: auto;
display: inline-block;

.icon {
width: 40px;
Expand Down
Loading