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
24 changes: 24 additions & 0 deletions packages/ui/app/(app)/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ChatWindow from '@/components/ChatWindow';
import { Metadata } from 'next';
import { Suspense } from 'react';

export const metadata: Metadata = {
title: 'Chat - Ask Starknet',
description: 'AI-powered assistant for Starknet and Cairo.',
icons: {
icon: '/ask_logo_white_alpha.png',
},
};

const ChatPage = () => {
return (
<div className="content-wrapper">
<Suspense>
<ChatWindow />
</Suspense>
</div>
);
};

export default ChatPage;

149 changes: 149 additions & 0 deletions packages/ui/app/(app)/history/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use client';

import { useEffect, useState } from 'react';
import Link from 'next/link';
import DeleteChat from '@/components/DeleteChat';
import { StoredChat } from '@/components/ChatWindow';
import { trackHistoryPageViewed, trackHistoryItemClicked } from '@/lib/posthog';
import { v4 as uuidv4 } from 'uuid';
import { Search, X } from 'lucide-react';

const ChatHistory = () => {
const [chats, setChats] = useState<StoredChat[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [sessionId] = useState(() => uuidv4());
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
const [isClearAllHovered, setIsClearAllHovered] = useState(false);

useEffect(() => {
if (typeof window !== 'undefined') {
const storedChats = JSON.parse(localStorage.getItem('chats') || '[]');
setChats(storedChats);

// Track history page view
trackHistoryPageViewed(sessionId);
}
}, [sessionId]);

// Sort chats so the most recent (by chat.createdAt) comes first.
const sortedChats = [...chats].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);

// Filter chats based on search term.
const filteredChats = sortedChats.filter((chat) =>
(chat.title || `Chat ${chat.id}`)
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);

// Handle chat item click
const handleChatClick = (chatId: string) => {
trackHistoryItemClicked(chatId);
};

return (
<div className="min-h-screen flex items-start justify-center p-4 pt-24 pb-24">
<div className="w-full max-w-2xl lg:max-w-3xl">
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
<h1 className="text-2xl font-bold">Chat History</h1>
<div className="flex gap-0 mt-2 md:mt-0 items-center -space-x-1">
{/* Search icon that expands to input on hover */}
<div
className={`relative flex items-center transition-transform duration-200 ${
isClearAllHovered && !isSearchExpanded && !searchTerm ? '-translate-x-16' : ''
}`}
onMouseEnter={() => setIsSearchExpanded(true)}
onMouseLeave={() => {
if (searchTerm === '') {
setIsSearchExpanded(false);
}
}}
>
{(isSearchExpanded || searchTerm) && (
<Search
className="absolute left-2 text-gray-500 dark:text-gray-400 z-10 pointer-events-none"
size={20}
/>
)}
<input
type="text"
placeholder="Search chats..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className={`transition-all duration-300 ease-in-out pl-9 pr-3 py-1.5 rounded-md bg-light-secondary dark:bg-dark-secondary focus:outline-none focus:ring-0 border-0 ${
isSearchExpanded || searchTerm
? 'w-64 opacity-100'
: 'w-8 opacity-0'
}`}
style={{
background: isSearchExpanded || searchTerm ? '' : 'transparent',
}}
/>
{!isSearchExpanded && !searchTerm && (
<div className="absolute inset-0 flex items-center justify-center cursor-pointer">
<div className="w-8 h-8 flex items-center justify-center">
<Search size={20} className="text-gray-600 dark:text-gray-400" />
</div>
</div>
)}
</div>

{/* Clear All button as red X icon with text on hover */}
<div
className="relative group"
onMouseEnter={() => setIsClearAllHovered(true)}
onMouseLeave={() => setIsClearAllHovered(false)}
>
<span className="absolute right-full mr-2 top-1/2 -translate-y-1/2 text-sm text-red-500 font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap pointer-events-none">
Clear All
</span>
<button
onClick={() => {
if (window.confirm('Clear all chat history?')) {
localStorage.removeItem('chats');
setChats([]);
}
}}
className="w-8 h-8 flex items-center justify-center transition-transform duration-200 hover:scale-110"
aria-label="Clear all chats"
>
<X size={20} className="text-red-500 dark:text-red-400" />
</button>
</div>
</div>
</div>
{filteredChats.length === 0 ? (
<p>No chats found.</p>
) : (
<ul className="space-y-4">
{filteredChats.map((chat) => (
<li
key={chat.id}
className="flex items-center justify-between p-4 bg-light-secondary dark:bg-dark-secondary rounded-lg border border-light-200 dark:border-dark-200"
>
<div>
<Link
href={`/c/${chat.id}`}
className="text-lg font-medium"
onClick={() => handleChatClick(chat.id)}
>
{chat.title || `Chat ${chat.id}`}
</Link>
{chat.createdAt && (
<p className="text-sm text-gray-500">
{new Date(chat.createdAt).toLocaleString()}
</p>
)}
</div>
<DeleteChat chatId={chat.id} chats={chats} setChats={setChats} />
</li>
))}
</ul>
)}
</div>
</div>
);
};

export default ChatHistory;
12 changes: 12 additions & 0 deletions packages/ui/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';

import Sidebar from '@/components/Sidebar';

export default function AppLayout({
children,
}: {
children: React.ReactNode;
}) {
return <Sidebar>{children}</Sidebar>;
}

187 changes: 186 additions & 1 deletion packages/ui/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,81 @@
body {
position: relative;
min-height: 100vh;
background-color: #000; /* Set a dark background color */
}

body.dark {
background-color: #000;
}

body:not(.dark) {
background-color: #fcfcf9;
}

.content-wrapper {
position: relative;
z-index: 1;
min-height: 100vh;
animation: fadeIn 0.8s ease-in-out;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: scale(1.05);
}
to {
opacity: 1;
transform: scale(1);
}
}

@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}

.animate-slideInLeft {
animation: slideInLeft 0.6s ease-out;
animation-fill-mode: forwards;
}

@keyframes slideInBottom {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.animate-slideInBottom {
animation: slideInBottom 0.6s ease-out;
}

@keyframes morphToMessage {
0% {
opacity: 0;
transform: translateY(200px) scale(0.8);
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}

.animate-morphToMessage {
animation: morphToMessage 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

@keyframes rotate {
Expand Down Expand Up @@ -52,3 +120,120 @@ body {
opacity: 0.2;
}
}

@keyframes scroll-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-33.333%);
}
}

@keyframes scroll-right {
0% {
transform: translateX(-33.333%);
}
100% {
transform: translateX(0);
}
}

.animate-scroll-left {
animation: scroll-left 50s linear infinite;
}

.animate-scroll-right {
animation: scroll-right 50s linear infinite;
}

@keyframes float {
0%,
100% {
transform: translateY(0px) translateX(0px);
}
25% {
transform: translateY(-10px) translateX(5px);
}
50% {
transform: translateY(-5px) translateX(-5px);
}
75% {
transform: translateY(-15px) translateX(3px);
}
}

.animate-float {
animation: float 8s ease-in-out infinite;
}

@keyframes float1 {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-15px) translateX(8px); }
50% { transform: translateY(-8px) translateX(-6px); }
75% { transform: translateY(-12px) translateX(4px); }
}

@keyframes float2 {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-8px) translateX(-10px); }
50% { transform: translateY(-18px) translateX(5px); }
75% { transform: translateY(-5px) translateX(-8px); }
}

@keyframes float3 {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-12px) translateX(-5px); }
50% { transform: translateY(-6px) translateX(8px); }
75% { transform: translateY(-16px) translateX(-3px); }
}

@keyframes float4 {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-10px) translateX(10px); }
50% { transform: translateY(-14px) translateX(-4px); }
75% { transform: translateY(-7px) translateX(6px); }
}

@keyframes float5 {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-9px) translateX(-7px); }
50% { transform: translateY(-15px) translateX(9px); }
75% { transform: translateY(-11px) translateX(-5px); }
}

@keyframes float6 {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-13px) translateX(6px); }
50% { transform: translateY(-7px) translateX(-9px); }
75% { transform: translateY(-17px) translateX(2px); }
}

.animate-float1 { animation: float1 8s ease-in-out infinite; }
.animate-float2 { animation: float2 8s ease-in-out infinite; }
.animate-float3 { animation: float3 8s ease-in-out infinite; }
.animate-float4 { animation: float4 8s ease-in-out infinite; }
.animate-float5 { animation: float5 8s ease-in-out infinite; }
.animate-float6 { animation: float6 8s ease-in-out infinite; }

/* Mobile-specific responsive improvements */
@media (max-width: 640px) {
body {
font-size: 14px;
}

.content-wrapper {
padding-bottom: 60px; /* Space for bottom navigation */
}
}

/* Prevent horizontal scrolling on small screens */
@media (max-width: 768px) {
body {
overflow-x: hidden;
}

* {
-webkit-tap-highlight-color: transparent;
}
}
Loading