Skip to content

refactor: Made better UI to differentiate User and Bolty chats, along with Scroll Area and Code snippet with copy functionality #17

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 5 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
169 changes: 112 additions & 57 deletions mobile-magic/apps/frontend/app/project/[projectId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

import { WORKER_URL } from "@/config";
import { Send } from "lucide-react";
import { Check, ClipboardCopy, Paperclip, Send } from "lucide-react";
import { usePrompts } from "@/hooks/usePrompts";
import { useActions } from "@/hooks/useActions";
import axios from "axios";
Expand All @@ -12,75 +12,130 @@ import { useAuth, useUser } from "@clerk/nextjs";
import { WORKER_API_URL } from "@/config";
import { ProjectsDrawer } from "@/components/ProjectsDrawer";
import Image from "next/image";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";

interface Action {
id: string;
content?: string;
code?: string;
}

export default function ProjectPage({ params }: { params: { projectId: string } }) {
const { prompts } = usePrompts(params.projectId);
const { actions } = useActions(params.projectId);
const [prompt, setPrompt] = useState("");
const { getToken } = useAuth();
const {user} = useUser()
const { user } = useUser()
const [copied, setCopied] = useState<string | null>(null);

const submitPrompt = async () => {
const token = await getToken();
axios.post(
`${WORKER_API_URL}/prompt`,
{
projectId: params.projectId,
prompt: prompt,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
setPrompt("");
};
const token = await getToken();
axios.post(
`${WORKER_API_URL}/prompt`,
{
projectId: params.projectId,
prompt: prompt,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
setPrompt("");
};

const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text);
setCopied(id);
setTimeout(() => setCopied(null), 2000);
};

return (
<div className="flex h-screen">
<div className="w-1/4 flex flex-col px-4 border-r border-gray-800">
<div className="sticky top-0 bg-background p-5 text-lg border-b border-gray-800">
Chat History
</div>
<div className="sticky h-4/5 my-5 flex-1 overflow-y-auto">
<ScrollArea className="pr-4">
<div className="space-y-6">
{prompts.filter((prompt) => prompt.type === "USER").map((prompt, index) => (
<div key={prompt.id} className="space-y-4">
<div className="flex bg-gray-800 p-3 rounded-lg border-4 border-gray-800 items-start gap-3">
<Image src={user?.imageUrl || ""} width={10} height={10} alt="Profile picture" className="rounded-full w-6 h-6" />
{prompt.content}
</div>

{actions[index] && (
<div className="flex items-start l:pl-7 gap-3">
<div className="flex-1">
<div className="text-gray-200 mb-3">{actions[index]?.content}</div>

return <div>
<div className="flex pt-16 flex-col md:flex-row">
<div className="w-full md:w-1/4 h-[93vh] flex flex-col justify-between p-4">
<div className="pt-4">
<Button variant={"ghost"} className="bg-primary-foreground rounded-full">
Chat history
</Button>
<div className="mx-auto py-3">
<div className="flex flex-col gap-3">
{prompts.filter((prompt) => prompt.type === "USER").map((prompt) => (
<span key={prompt.id} className="flex gap-2 py-3 px-2 border rounded bg-secondary">
<Image src={user?.imageUrl || ""} width={10} height={10} alt="Profile picture" className="rounded-full w-6 h-6" />
{prompt.content}
</span>
{('code' in actions[index] && actions[index]?.code) && (
<div className="relative flex bg-gray-800 justify-between rounded-md overflow-hidden mb-4">

<pre className="md:p-4 overflow-x-auto text-sm max-h-60 scrollbar-thin scrollbar-thumb-gray-600">
<code className="text-gray-200 whitespace-pre-wrap break-words">
{String(actions[index].code || "")}
</code>
</pre>
<span className="flex items-center justify-between xl:px-4 py-2 text-xs text-gray-100">
<Button
variant="ghost"
size="sm"
className="h-6 right-1 w-6 p-0"
onClick={() => copyToClipboard(
String(actions[index].code || ""),
`header-${actions[index].id}`
)}
>
{copied === `header-${actions[index].id}` ? (
<Check className="h-3.5 w-3.5 text-green-500" />
) : (
<ClipboardCopy className="h-3.5 w-3.5 text-gray-400" />
)}
</Button>
</span>
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
{actions.map((action) => (
<div key={action.id}>
{action.content}
</div>
))}
</div>
</ScrollArea>
</div>
<div className="sticky bottom-5 bg-background pt-4 border-gray-800">
<div className="flex flex-col rounded-lg overflow-hidden border border-primary/20">
<Textarea
onChange={e => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
submitPrompt();
}
}}
className="border-0 bg-transparent text-primary focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-gray-400 max-h-44 resize-none"
/>
<div className="flex items-end justify-end gap-2 pr-1">
<Button variant="ghost" className="border-gray-800 p-2 h-9 w-9">
<Paperclip className="w-5 h-5 text-gray-500 " />
</Button>
<Button
className="rounded-sm px-4 py-2"
onClick={submitPrompt}>
<Send />
</Button>
</div>
</div>
</div>
<div className="flex gap-2 pb-8">
<Input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Type a message"
onKeyDown={(e) => {
if (e.key === 'Enter') {
submitPrompt();
}
}}
/>
<Button onClick={submitPrompt}>
<Send />
</Button>
</div>
</div>
<div className="md:w-3/4 p-8">
<iframe src={`${WORKER_URL}/`} width={"100%"} height={"100%"} title="Project Worker" className="rounded-lg" />
<div className="md:w-3/4 p-0">
<iframe src={`${WORKER_URL}/`} width={"100%"} height={"100%"} title="Project Worker" className="border-none rounded-lg" />
</div>
<ProjectsDrawer />
</div>
<ProjectsDrawer />
</div>
);
}
107 changes: 69 additions & 38 deletions mobile-magic/apps/frontend/components/ProjectsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer"
import { BACKEND_URL } from "@/config";
import axios from "axios";
import { useAuth } from "@clerk/nextjs";
import { SignInButton, useAuth } from "@clerk/nextjs";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Input } from "./ui/input";
import { LogOutIcon, MessageSquareIcon } from "lucide-react";
import { LogOutIcon, MessageCircleIcon, MessageSquareIcon, SearchIcon } from "lucide-react";
import { Button } from "./ui/button";

const WIDTH = 250;
Expand Down Expand Up @@ -62,8 +63,18 @@ export function ProjectsDrawer() {
const projects = useProjects();
const [isOpen, setIsOpen] = useState(false);
const [searchString, setSearchString] = useState("");
const { getToken } = useAuth();
const [token, setToken] = useState<string | null>(null);
const router = useRouter();

// To get whether the user is logged in or not
useEffect(() => {
(async () => {
const fetchedToken = await getToken();
setToken(fetchedToken);
})();
}, [getToken]);

useEffect(() => {
// track mouse pointer, open if its on the left ovver the drawer
const handleMouseMove = (e: MouseEvent) => {
Expand All @@ -89,42 +100,62 @@ export function ProjectsDrawer() {
<Button onClick={() => {
setIsOpen(false);
}} variant="ghost" className="w-full"><MessageSquareIcon /> Start new project</Button>
<Input
type="text"
placeholder="Search"
value={searchString}
onChange={(e) => setSearchString(e.target.value)}
/>
<DrawerTitle className="font-semibold pl-2 pt-2">Your projects</DrawerTitle>
{Object.keys(projects).map((date) => (
<div key={date} className="py-2">
<h2 className="font-semibold text-xs px-2">{date}</h2>
{projects[date]
.filter((project) =>
project.description
.toLowerCase()
.includes(searchString.toLowerCase()),
)
.map((project) => (
<Button
key={project.id}
variant={"ghost"}
onClick={() => {
router.push(`/project/${project.id}`);
}}
className="pl-2 w-full text-left justify-start items-start rounded hover:bg-accent cursor-pointer hover:text-accent-foreground text-muted-foreground"
>
{project.description}
</Button>
))}
</div>
))}
</DrawerHeader>
<DrawerFooter>
<Button variant="ghost" className="w-full">
<LogOutIcon /> Logout
</Button>
</DrawerFooter>

{ token && (
<div>
<Input
type="text"
placeholder="Search"
value={searchString}
onChange={(e) => setSearchString(e.target.value)}
/>
<DrawerTitle className="text-[12px] py-2">Your projects</DrawerTitle>
<div className="flex space-between border rounded-md pr-2 pl-1">
<input className="text-[12px] w-full p-1 text-sm border-none ouline-none" type="text" placeholder="Search" value={searchString} onChange={(e) => setSearchString(e.target.value)} >
</input>
<div className="flex items-center">
<SearchIcon className="w-4 h-auto" />
</div>
</div>
</div>
)}
{token && Object.keys(projects).map((date) => (
<div key={date}>
<h2 className="text-[10px]">{date}</h2>
{projects[date].filter((project) => project.description.toLowerCase().includes(searchString.toLowerCase())).map((project) => (
<div key={project.id} className="my-1">
<Button variant={"outline"} onClick={() => {
router.push(`/project/${project.id}`);
}} className="border pl-1 w-full rounded hover:bg-accent cursor-pointer hover:text-accent-foreground text-[12px]">
<div className="w-full flex">

<div className="pl-2 flex items-center"><MessageCircleIcon className="w-4 h-4" /></div> <div className="pl-2">{project.description}</div>
</div>
</Button >
</div>
))}
</div>
))}

</DrawerHeader>
<DrawerDescription>
{!token && (
<div className="flex flex-col w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-1">
<span className="bg-primary/10 w-full p-1 rounded-md shadow-xs hover:bg-primary/10 text-center">
<SignInButton />
</span> to see your projects
</div>
</div>
)}
</DrawerDescription>
<DrawerFooter>
{token && (
<Button variant="ghost" className="w-full">
<LogOutIcon /> Logout
</Button>
)}
</DrawerFooter>
</DrawerContent>
</Drawer>
);
Expand Down
7 changes: 6 additions & 1 deletion mobile-magic/apps/frontend/components/Prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ export function Prompt() {
placeholder="Create a chess application..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="py-4 pl-4 pr-12 min-h-28 max-h-40 overflow-auto"
className="py-4 pl-4 pr-12 min-h-28 max-h-40 overflow-auto resize-none"
/>
{prompt && (
<div>
<Button variant="ghost" className="absolute top-4 right-16 cursor-pointer border-gray-800 p-2 h-9 w-9">
<Paperclip className="w-5 h-5 text-gray-500 " />
</Button>
<Button
className="absolute top-4 right-4 cursor-pointer"
onClick={async () => {
Expand All @@ -53,6 +57,7 @@ export function Prompt() {
}}>
<Send />
</Button>
</div>
)}
<div className="max-w-2xl mx-auto pt-4">
<TemplateButtons onTemplateClick={handleTemplateClick} />
Expand Down
Loading