Skip to content
Merged
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
51 changes: 24 additions & 27 deletions frontend/src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,35 @@
import { Progress } from "@/components/ui/progress";

interface ProgressBarProps {
currentStep: number;
totalSteps?: number;
labels?: string[];
}

export default function ProgressBar({ currentStep, totalSteps = 4, labels = ['Connect', 'Graph', 'Configure', 'Launch'] }: ProgressBarProps) {
export default function ProgressBar({
currentStep,
totalSteps = 4,
labels = ["Connect", "Graph", "Configure", "Launch"],
}: ProgressBarProps) {
const progressValue =
totalSteps > 1 ? ((currentStep - 1) / (totalSteps - 1)) * 100 : currentStep === 1 ? 0 : 100;

return (
<div className="w-full bg-muted border-b border-border px-4 py-3">
<div className="max-w-2xl mx-auto flex items-center justify-between">
{Array.from({ length: totalSteps }).map((_, i) => (
<div key={i} className="flex items-center">
<div className="flex flex-col items-center">
<div
className={`w-3 h-3 rounded-full transition-colors ${
i + 1 < currentStep
? 'bg-primary'
: i + 1 === currentStep
? 'bg-primary glow-teal-sm'
: 'bg-accent'
}`}
/>
<span className={`text-[10px] font-mono mt-1 hidden sm:block ${
i + 1 <= currentStep ? 'text-primary' : 'text-muted-foreground'
}`}>
{labels[i]}
</span>
</div>
{i < totalSteps - 1 && (
<div className={`w-12 sm:w-20 h-px mx-2 ${
i + 1 < currentStep ? 'bg-primary' : 'bg-accent'
}`} />
)}
</div>
))}
<div className="max-w-2xl mx-auto space-y-3">
<Progress value={progressValue} max={100} type="default" />
<div className="flex items-center justify-between">
{labels.map((label, i) => (
<span
key={i}
className={`text-[10px] font-mono hidden sm:block transition-colors duration-300 ${
i + 1 <= currentStep ? "text-primary" : "text-muted-foreground"
}`}
>
{label}
</span>
))}
</div>
</div>
</div>
);
Expand Down
78 changes: 58 additions & 20 deletions frontend/src/components/ui/progress.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,61 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";

import React from "react";
import { cn } from "@/lib/utils";

const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
type TProgressType = "default" | "success" | "warning" | "error" | "secondary";

interface ProgressProps extends React.HTMLAttributes<HTMLProgressElement> {
value: number;
max?: number;
colors?: { [key: string]: string };
type?: TProgressType;
}

export { Progress };
const getColor = (value: number, type: TProgressType, colors?: Record<string, string>) => {
if (colors) {
const keys = Object.keys(colors).sort((a, b) => parseInt(a) - parseInt(b));
for (let i = keys.length - 1; i >= 0; i--) {
if (value >= parseInt(keys[i])) {
return colors[keys[i]];
}
}
}
switch (type) {
case "success":
return "hsl(var(--primary))";
case "error":
return "hsl(var(--destructive))";
case "warning":
return "hsl(var(--warning))";
case "secondary":
return "hsl(var(--muted-foreground))";
default:
return "hsl(var(--primary))";
}
};

export const Progress = ({
value,
max = 100,
colors,
type = "default",
className,
...props
}: ProgressProps) => {
const fillColor = getColor(value, type, colors);

return (
<progress
value={value}
max={max}
className={cn(
"progress-step h-2.5 w-full appearance-none border-none",
"[&::-webkit-progress-bar]:rounded-[5px] [&::-webkit-progress-bar]:bg-muted",
"[&::-webkit-progress-value]:rounded-[5px] [&::-webkit-progress-value]:transition-all [&::-webkit-progress-value]:duration-300 [&::-webkit-progress-value]:ease-out",
"[&::-moz-progress-bar]:rounded-[5px] [&::-moz-progress-bar]:transition-all [&::-moz-progress-bar]:duration-300 [&::-moz-progress-bar]:ease-out",
className
)}
style={{ ["--progress-fill" as string]: fillColor } as React.CSSProperties}
{...props}
/>
);
};
8 changes: 8 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,11 @@
.tabular-nums {
font-variant-numeric: tabular-nums;
}

/* Smooth progress bar - uses --progress-fill from inline style */
.progress-step::-webkit-progress-value {
background: var(--progress-fill, hsl(var(--primary)));
}
.progress-step::-moz-progress-bar {
background: var(--progress-fill, hsl(var(--primary)));
}
82 changes: 49 additions & 33 deletions frontend/src/pages/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Navbar from '../components/Navbar';
import ProgressBar from '../components/ProgressBar';
import StepNav from '../components/StepNav';
import ForceGraph, { DEFAULT_NODES, DEFAULT_EDGES } from '../components/ForceGraph';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '../components/ui/resizable';

interface ChatMessage {
role: 'ai' | 'user';
Expand Down Expand Up @@ -31,6 +32,7 @@ export default function Graph() {
const [typing, setTyping] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);
const [graphKey, setGraphKey] = useState(0);
const hasUserSentMessage = messages.some(m => m.role === 'user');

useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
Expand All @@ -51,24 +53,23 @@ export default function Graph() {
};

return (
<div className="min-h-screen bg-background flex flex-col">
<div className="h-screen bg-background flex flex-col overflow-hidden">
<Navbar />
<div className="pt-14">
<div className="pt-14 shrink-0">
<ProgressBar currentStep={2} />
</div>

<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className="flex-1 flex flex-col md:flex-row overflow-hidden"
className="flex-1 min-h-0 flex overflow-hidden"
>
{/* Left Panel — Chat */}
<div className="w-full md:w-[40%] flex flex-col bg-muted border-r border-border" style={{ height: 'calc(100vh - 56px - 41px - 53px)' }}>
{/* Chat Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={40} minSize={25} maxSize={70} className="flex flex-col min-w-0 bg-muted">
{/* Chat Header */}
<div className="flex shrink-0 items-center justify-between px-4 py-3 border-b border-border">
<div className="flex items-center gap-2">
<span className="text-sm">🧠</span>
<span className="text-sm font-semibold text-foreground">ArbOS Brain</span>
</div>
<span className="font-mono text-[10px] px-2 py-0.5 rounded bg-secondary/20 text-secondary border border-secondary/30">
Expand All @@ -77,7 +78,7 @@ export default function Graph() {
</div>

{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
<div className="flex-1 min-h-0 overflow-y-auto p-4 space-y-3">
{messages.map((msg, i) => (
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div
Expand All @@ -104,7 +105,7 @@ export default function Graph() {
</div>

{/* Stats */}
<div className="px-4 py-2 border-t border-border">
<div className="shrink-0 px-4 py-2 border-t border-border">
<div className="font-mono text-[10px] text-muted-foreground flex gap-4">
<span>Nodes: <span className="text-primary">14</span></span>
<span>Edges: <span className="text-primary">23</span></span>
Expand All @@ -113,7 +114,7 @@ export default function Graph() {
</div>

{/* Input */}
<div className="p-3 border-t border-border">
<div className="shrink-0 p-3 border-t border-border">
<div className="flex gap-2">
<input
value={input}
Expand All @@ -134,31 +135,45 @@ export default function Graph() {
Try: /add crypto · /remove · /scan · /suggest
</p>
</div>
</div>

{/* Right Panel — Graph */}
<div className="w-full md:w-[60%] bg-background relative" style={{ height: 'calc(100vh - 56px - 41px - 53px)', minHeight: '400px' }}>
<ForceGraph
key={graphKey}
nodes={DEFAULT_NODES}
edges={DEFAULT_EDGES}
width={700}
height={500}
animated
/>
{/* Legend */}
<div className="absolute bottom-4 left-4 bg-card/80 backdrop-blur border border-border rounded-md px-3 py-2 text-[10px] font-mono text-muted-foreground space-y-1">
<div>── IMPLIES &nbsp; -- EXCLUSIVE &nbsp; ·· PARTITION</div>
<div>
<span className="text-primary">●</span> Active Arb &nbsp;
<span className="text-muted-foreground">●</span> Monitored &nbsp;
<span className="text-destructive">●</span> Low Liquidity
</ResizablePanel>

<ResizableHandle withHandle className="bg-border hover:bg-primary/20 transition-colors" />

{/* Right Panel — Graph */}
<ResizablePanel defaultSize={60} minSize={30} maxSize={75} className="min-w-0 bg-background">
<div className="w-full h-full bg-background relative flex items-center justify-center" style={{ minHeight: '400px' }}>
{hasUserSentMessage ? (
<>
<ForceGraph
key={graphKey}
nodes={DEFAULT_NODES}
edges={DEFAULT_EDGES}
width={700}
height={500}
animated
/>
{/* Legend */}
<div className="absolute bottom-4 left-4 bg-card/80 backdrop-blur border border-border rounded-md px-3 py-2 text-[10px] font-mono text-muted-foreground space-y-1">
<div>── IMPLIES &nbsp; -- EXCLUSIVE &nbsp; ·· PARTITION</div>
<div>
<span className="text-primary">●</span> Active Arb &nbsp;
<span className="text-muted-foreground">●</span> Monitored &nbsp;
<span className="text-destructive">●</span> Low Liquidity
</div>
</div>
</>
) : (
<p className="text-sm text-muted-foreground font-mono">
Type something in the chat to build your graph
</p>
)}
</div>
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</motion.div>

<StepNav
<div className="shrink-0">
<StepNav
backTo="/connect"
backLabel="← Back"
nextTo="/configure"
Expand All @@ -169,6 +184,7 @@ export default function Graph() {
</span>
}
/>
</div>
</div>
);
}