Skip to content

eric/ui-improvement #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
node_modules
playwright-report
test-results
.vscode

# User configuration
user_config.yaml
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.PHONY: dev/server
dev/server:
go run github.com/goware/rerun/cmd/rerun@latest \
-watch . \
-watch cmd \
-watch internal \
-ignore app \
-run "go run ./cmd/api"

Expand Down
58 changes: 58 additions & 0 deletions app/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,61 @@
@apply bg-background text-foreground;
}
}

@keyframes subtle-pulse {
0%,
100% {
border-color: rgb(59 130 246);
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
}
50% {
border-color: rgb(59 130 246 / 0.7);
box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
}
}
.animate-pulse-subtle {
animation: subtle-pulse 3s ease-in-out infinite;
}

.shiny-text {
color: #b5b5b5a4; /* Adjust this color to change intensity/style */
background: linear-gradient(
120deg,
rgba(255, 255, 255, 0) 40%,
rgba(0, 0, 0, 1) 50%,
rgba(255, 255, 255, 0) 60%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
display: inline-block;
animation: shine 5s linear infinite;
}

@keyframes shine {
0% {
background-position: 100%;
}
100% {
background-position: -100%;
}
}

.shiny-text.disabled {
animation: none;
}

@keyframes fade-in {
from {
opacity: 0;
filter: blur(8px);
}
to {
opacity: 1;
filter: blur(0px);
}
}

.animate-fade-in {
animation: fade-in 1.5s ease-out forwards;
}
2 changes: 1 addition & 1 deletion app/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Providers>
<main className="max-w-screen-xl mx-auto p-6">{children}</main>
<main className="mx-auto p-6">{children}</main>
<Toaster />
</Providers>
</body>
Expand Down
17 changes: 17 additions & 0 deletions app/app/ui-demo/feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface FeedbackProps {
feedback: string;
className?: string;
}

export default function Feedback({ feedback, className }: FeedbackProps) {
if (!feedback) {
return null;
}

return (
<div className={className}>
<h2 className="text-lg font-semibold mb-2">Current feedback:</h2>
<p>{feedback}</p>
</div>
);
}
248 changes: 248 additions & 0 deletions app/app/ui-demo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
"use client";

import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Message } from "@/lib/types";
import { Agent } from "@/components/agent";
import { Tool } from "@/components/tool";
import { TestFiles } from "./test-files";
import Feedback from "./feedback";

export default function WebSocketDemo() {
const [messages, setMessages] = useState<string[]>([]);
const [scenarioCount, setScenarioCount] = useState<number>(0);
const [fileNames, setFileNames] = useState<string[]>([]);
const [scenario, setScenario] = useState<string>("");
const [feedback, setFeedback] = useState<string>("");
const [inputValue, setInputValue] = useState(
"https://ai-hackathon-demo-delta.vercel.app/"
);
const [isConnected, setIsConnected] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null);

const addMessage = (message: string) => {
setMessages((prev) => [...prev, message]);
};

const handleClose = () => {
if (!wsRef.current) return;
wsRef.current.close();
};

const handlePause = () => {
if (!wsRef.current) return;
if (isPaused) {
wsRef.current.send("RESUME");
} else {
wsRef.current.send("PAUSE");
}
};

const handleSend = () => {
if (!wsRef.current) {
try {
console.log("Attempting to connect to WebSocket...");
wsRef.current = new WebSocket("ws://localhost:8080/ws");

wsRef.current.onopen = () => {
console.log("WebSocket connection opened successfully");
setIsConnected(true);
addMessage("OPEN");
addMessage(`SEND '${inputValue}'`);
if (wsRef.current) {
wsRef.current.send(inputValue);
}
};

wsRef.current.onclose = (event) => {
console.log("WebSocket connection closed:", event.code, event.reason);
setIsConnected(false);
setIsPaused(false);
addMessage(
`CLOSE '${event.code}${
event.reason ? `, Reason: ${event.reason}` : ""
}'`
);
wsRef.current = null;
};

wsRef.current.onmessage = (evt) => {
console.log(evt.data);
addMessage(evt.data);
const id = evt.data.split(" ")[0];
if (id === "SCENARIOS") {
const numScenarios = parseInt(evt.data.split(" ")[1]);
setScenarioCount(numScenarios);
}
if (id === "FILENAME") {
const fileName = evt.data.split(" ")[1];
setFileNames((prev) => [...prev, fileName]);
}
if (id === "SCENARIO") {
const scenario = evt.data.substring(evt.data.indexOf(" ") + 1);
setScenario(scenario);
}
if (id === "FEEDBACK") {
const feedback = evt.data.substring(evt.data.indexOf(" ") + 1);
setFeedback(feedback);
}
if (id === "STATUS") {
const status = evt.data.split(" ")[1];
setIsPaused(status === "Paused");
}
};

wsRef.current.onerror = (error) => {
console.error("WebSocket error:", error);
addMessage(
`ERROR 'Connection failed - check browser console for details'`
);
};
} catch (error: any) {
console.error("WebSocket connection error:", error);
addMessage(
`ERROR 'Failed to connect to WebSocket server - ${error.message}'`
);
}
return;
}
addMessage(`SEND: ${inputValue}`);
wsRef.current.send(inputValue);
};

useEffect(() => {
if (scrollAreaRef.current) {
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight;
}
}, [messages]);

return (
<div className="container mx-auto p-6">
<Card className="w-4xl mx-auto mb-12">
<CardHeader className="pb-0">
<CardTitle>End-to-end Test Generation</CardTitle>
<CardDescription>
Generate end-to-end tests that are passing with just a prompt and
the website URL.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
<div className="space-y-4">
<div className="flex gap-2"></div>
<div className="flex gap-2">
<Textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
disabled={isConnected}
placeholder="Write a the website's URL, yeah, that's it."
/>
<div className="flex flex-col gap-2">
<Button
onClick={handleSend}
disabled={isConnected}
variant="default"
>
Send
</Button>
<Button
onClick={handlePause}
disabled={!isConnected}
variant={isPaused ? "default" : "secondary"}
>
{isPaused ? "Resume" : "Pause"}
</Button>
<Button
onClick={handleClose}
disabled={!isConnected}
variant="destructive"
>
Close
</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="flex flex-row gap-8 justify-center">
<Agent
title="Analyzer"
description="Analyzes the website and generates scenarios."
active={
isActive(messages, "ANALYZER") || isActive(messages, "TOOLCALL")
}
messages={getMessages(messages, ["ANALYZER", "TOOLCALL"])}
/>
<Agent
title="Generator"
description="Generates end-to-end test from scenarios."
active={isActive(messages, "GENERATOR")}
messages={getMessages(messages, ["GENERATOR"])}
/>
<Agent
title="Evaluator"
description="Evaluates the generated end-to-end test."
active={isActive(messages, "EVALUATOR")}
messages={getMessages(messages, ["EVALUATOR"])}
/>
</div>
<div className="flex flex-row gap-8 justify-center pt-16">
<Tool
title="get_sitemap_tool"
description="Gets the content of the website."
active={isActive(messages, "GET_SITEMAP_TOOL")}
messages={getMessages(messages, ["GET_SITEMAP_TOOL"])}
/>
<Tool
title="get_content_tool"
description="Gets the content of passed urls."
active={isActive(messages, "GET_CONTENT_TOOL")}
messages={getMessages(messages, ["GET_CONTENT_TOOL"])}
/>
<div className="w-[470px]">
<Feedback feedback={feedback} />
</div>
<Tool
title="run_test_tool"
description="Runs the generated end-to-end test."
active={isActive(messages, "RUN_TEST_TOOL")}
messages={getMessages(messages, ["RUN_TEST_TOOL"])}
/>
</div>
<TestFiles
className="pt-24"
count={scenarioCount}
names={fileNames}
scenario={scenario}
/>
</div>
);
}

const isActive = (messages: string[], id: string): boolean => {
return (
messages.length > 0 && messages[messages.length - 1].split(" ")[0] === id
);
};

const getMessages = (messages: string[], ids: string[]): Message[] => {
const agentName = ids[0];
return messages
.filter((message) => ids.includes(message.split(" ")[0]))
.map((message) => {
const id = message.split(" ")[0];
const description = message.split(" ").slice(1).join(" ");
return { title: id === agentName ? "Agent" : "Toolcall", description };
});
};
Loading