added localStorage functionality to the entire project, LLM keeps getting overloaded

This commit is contained in:
Ivan Rudik
2025-11-09 09:24:47 +00:00
parent 6421160279
commit 64be41f894
10 changed files with 447 additions and 702 deletions
+66 -123
View File
@@ -1,24 +1,46 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation"; import { getStoredProjects, StoredProject } from "../utils/storage";
import Link from 'next/link'; import Link from 'next/link';
function Dashboard() { // 2. Changed from 'export default function' export default function Dashboard() {
const router = useRouter(); const [query, setQuery] = useState("");
const [query, setQuery] = useState(""); const [sideBarOpen, setSidebarOpen] = useState(true);
const [sideBarOpen, setSidebarOpen] = useState(true); const [sortOrder, setSortOrder] = useState("newest");
const [projects, setProjects] = useState<StoredProject[]>([]);
const projects = [ useEffect(() => {
{ name: "Alpha Project", createdAt: "2025-01-01" }, const storedProjects = getStoredProjects();
{ name: "Beta Board", createdAt: "2025-02-15" }, setProjects(storedProjects);
{ name: "Gamma Tasl", createdAt: "2025-03-10" }, }, []);
];
const filtered = projects.filter((p) => // 5. Sorting logic now uses the state variable
p.name.toLowerCase().includes(query.toLowerCase()) const sortedProjects = [...projects].sort((a, b) => {
); const dateA = new Date(a.id).getTime(); // Use 'id' (timestamp) for sorting
const dateB = new Date(b.id).getTime();
if (sortOrder === "newest") {
return dateB - dateA; // Newest first
} else {
return dateA - dateB; // Oldest first
}
});
// 6. Filter the sorted projects
const filtered = sortedProjects.filter((p) =>
p.title.toLowerCase().includes(query.toLowerCase())
);
// 7. Fix the "Create" button link
const handleCreateClick = () => {
window.location.href = "/formfile"; // Changed from "/dashbaord/create"
};
const toggleSortOrder = () => {
setSortOrder(currentOrder => (currentOrder === "newest" ? "oldest" : "newest"));
};
<<<<<<< HEAD
return ( return (
<div className="flex min-h-screen bg-zinc-50 dark:bg-black font-sans transition-all duration-300"> <div className="flex min-h-screen bg-zinc-50 dark:bg-black font-sans transition-all duration-300">
<aside <aside
@@ -55,13 +77,6 @@ function Dashboard() { // 2. Changed from 'export default function'
> >
Settings Settings
</p> </p>
{/* 3. Added the Logout link here */}
<Link
href="/auth/logout"
className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
Logout
</Link>
</nav> </nav>
</aside> </aside>
@@ -81,7 +96,7 @@ function Dashboard() { // 2. Changed from 'export default function'
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => router.push("/dashbaord/create")} onClick={handleCreateClick}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md shadow-sm" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md shadow-sm"
> >
Create Create
@@ -94,111 +109,39 @@ function Dashboard() { // 2. Changed from 'export default function'
<h1 className="text-2xl font-semibold text-black dark:text-zinc-50 mb-4"> <h1 className="text-2xl font-semibold text-black dark:text-zinc-50 mb-4">
Project Names Project Names
</h1> </h1>
<button
type="button"
onClick={toggleSortOrder}
className="px-3 py-1 text-sm font-medium border rounded-md"
style={{
color: '#333',
backgroundColor: '#F7F7F7',
borderColor: '#D1D1D1'
}}
>
Sort by: {sortOrder === 'newest' ? 'Newest' : 'Oldest'}
</button>
<section> <section>
<ul className="space-y-3"> <ul className="space-y-3">
{filtered.map((project, index) => ( {filtered.map((project, index) => (
<li <li
key={index} key={project.id}
className="flex justify-between p-4 bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 rounded-md text-zinc-800 dark:text-zinc-100" className="flex justify-between p-4 bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 rounded-md text-zinc-800 dark:text-zinc-100"
=======
return (
<div className="flex min-h-screen bg-zinc-50 dark:bg-black font-sans transition-all duration-300">
<aside
className={`${sideBarOpen ? "w-64" : "w-16"
} bg-white dark:bg-zinc-900 border-r border-zinc-200 dark:border-zinc-800 transition-all duration-300 flex flex-col`}
>
<div className="flex items-center justify-between p-4 border-b border-zinc-200 dark:border-zinc-800">
{sideBarOpen && (
<span className="text-lg font-semibold text-zinc-800 dark:text-zinc-100">
More
</span>
)}
<button
onClick={() => setSidebarOpen(!sideBarOpen)}
className="text-zinc-600 dark:text-zinc-300 hover:text-blue-500 transition-colors"
title="Toggle Sidebar"
>
☰
</button>
</div>
<nav
className={`flex flex-col p-4 gap-3 text-zinc-700 dark:text-zinc-300 transition-opacity duration-200 ${sideBarOpen ? "opacity-100" : "opacity-0"
}`}
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
> >
<Link <span>{project.title}</span>
href="/" <span className="text-zinc-500 dark:text-zinc-400 text-sm">
className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors" Created: {new Date(project.id).toLocaleDateString()}
> </span>
Home </li>
</Link> ))}
<p {filtered.length === 0 && (
className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors" <li className="text-zinc-500">{projects.length > 0 ? "No results found" : "No projects yet. Click 'Create' to start!"}</li>
> )}
Settings </ul>
</p> </section>
{/* 3. Added the Logout link here */} </main>
<Link </div>
href="/auth/logout" </div>
className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors" );
>
Logout
</Link>
</nav>
</aside>
<div className="flex-1 flex flex-col min-w-0">
<header className="sticky top-0 z-10 bg-white dark:bg-black/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-800">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-4">
<div className="flex-1">
<input
id="dashboard-search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search projects, boards or tasks..."
className="w-full max-w-md px-4 py-2 rounded-md border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => router.push("/dashbaord/create")}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md shadow-sm"
>
Create
</button>
</div>
</div>
</header>
<main className="max-w-7xl w-full mx-auto px-6 py-10">
<h1 className="text-2xl font-semibold text-black dark:text-zinc-50 mb-4">
Project Names
</h1>
<section>
<ul className="space-y-3">
{filtered.map((project, index) => (
<li
key={index}
className="flex justify-between p-4 bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 rounded-md text-zinc-800 dark:text-zinc-100"
>
<span>{project.name}</span>
<span className="text-zinc-500 dark:text-zinc-400 text-sm">
Created: {project.createdAt}
</span>
</li>
))}
{filtered.length === 0 && (
<li className="text-zinc-500">No results found</li>
)}
</ul>
</section>
</main>
</div>
</div>
);
} }
+107 -32
View File
@@ -2,13 +2,14 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { motion, useMotionValue, useTransform, useAnimation } from "framer-motion"; import { motion, useMotionValue, useTransform, useAnimation } from "framer-motion";
import { useRouter } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { CardData } from "@/app/exportType/types";
// 1. Updated ColorWay interface // 1. Updated ColorWay interface
interface ColorWay { interface ColorWay {
card: string; card: string;
text: string; text: string;
titleColor: string; // Added title color titleColor: string;
} }
// 2. Updated COLOR_WAYS array // 2. Updated COLOR_WAYS array
@@ -21,21 +22,100 @@ const COLOR_WAYS: ColorWay[] = [
{ card: "#B8C0AD", text: "#173C46", titleColor: "#EA627F" }, { card: "#B8C0AD", text: "#173C46", titleColor: "#EA627F" },
]; ];
interface CardData { const parseAiResponse = (markdownString: string): CardData[] => {
id: number; const ideas: CardData[] = [];
title: string; const ideaBlocks = markdownString.split("### ").filter(block => block.trim() !== "");
summary: string;
tracks: string[]; ideaBlocks.forEach((block, index) => {
const titleMatch = block.match(/^(.*?)\n/);
const conceptMatch = block.match(/\*\*Concept:\*\*\s*(.*)/s);
// Simple track parsing: find all bolded text that isn't a main header
const tracksMatch = block.matchAll(/\*\*(?!Concept|Relevance|Core|Team|Bonus)(.*?):\*\*/g);
const tracks = [...tracksMatch].map(match => match[1].trim());
ideas.push({
id: index + 1,
title: titleMatch ? titleMatch[1].trim() : `Idea ${index + 1}`,
summary: conceptMatch ? conceptMatch[1].trim() : "No concept provided.",
tracks: tracks.length > 0 ? tracks : ["General"], // Fallback
});
});
return ideas;
};
export default function CardsPage() {
const [cards, setCards] = useState<CardData[]>([]);
const [initialCards, setInitialCards] = useState<CardData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
const searchParams = useSearchParams();
const projectId = searchParams.get('projectId');
const projectTitle = searchParams.get('projectTitle');
useEffect(() => {
if (typeof window === 'undefined') return;
// **** MODIFIED THIS SECTION ****
// Get the AI response from the form page
const aiResponse = localStorage.getItem('temp_aiResponse');
if (aiResponse) {
localStorage.removeItem('temp_aiResponse'); // Clean up
const parsedCards = parseAiResponse(aiResponse);
setCards(parsedCards.reverse());
setInitialCards(parsedCards.reverse()); // Save for reset
} else {
// Handle case where user lands here directly
// Maybe redirect or show an error
console.warn("No AI response found. Using hardcoded cards as fallback.");
const hardcodedCards: CardData[] = [
{ id: 1, title: "AI Project Idea 1", summary: "A cool project...", tracks: ["AI", "Web"] },
{ id: 2, title: "AI Project Idea 2", summary: "Another cool project...", tracks: ["Mobile", "Game"] },
];
setCards(hardcodedCards.reverse());
setInitialCards(hardcodedCards.reverse());
}
setIsLoading(false);
}, []); // Runs once on load
if (isLoading) {
return <div className="grid h-screen w-full place-items-center bg-orange-100"><p>Loading cards...</p></div>
}
if (!projectId || !projectTitle) {
return (
<div className="grid h-screen w-full place-items-center bg-orange-100">
<p>Missing project information. Please <a href="/formfile">start a new project</a>.</p>
</div>
)
}
return (
<SwipeCards
initialCards={initialCards}
currentCards={cards}
setCards={setCards}
projectId={projectId}
projectTitle={projectTitle}
/>
)
} }
const SwipeCards: React.FC<{ initialCards: CardData[] }> = ({ initialCards }) => { interface SwipeCardsProps {
const [cards, setCards] = useState<CardData[]>(initialCards); initialCards: CardData[];
const router = useRouter(); currentCards: CardData[];
setCards: React.Dispatch<React.SetStateAction<CardData[]>>;
projectId: string;
projectTitle: string;
}
// Update cards state when initialCards prop changes const SwipeCards: React.FC<SwipeCardsProps> = ({
useEffect(() => { initialCards,
setCards(initialCards); currentCards,
}, [initialCards]); setCards,
projectId,
projectTitle
}) => {
const router = useRouter();
const removeCard = () => { const removeCard = () => {
setCards((prev) => prev.slice(0, prev.length - 1)); setCards((prev) => prev.slice(0, prev.length - 1));
@@ -44,43 +124,43 @@ const SwipeCards: React.FC<{ initialCards: CardData[] }> = ({ initialCards }) =>
const handleAccept = (cardData: CardData) => { const handleAccept = (cardData: CardData) => {
removeCard(); removeCard();
// Redirect to chatbot with card data // Redirect to chatbot with card data
router.push(`/chatbot?title=${encodeURIComponent(cardData.title)}&summary=${encodeURIComponent(cardData.summary)}`); router.push(`/chatbot?projectId=${encodeURIComponent(projectId)}&projectTitle=${encodeURIComponent(projectTitle)}&cardTitle=${encodeURIComponent(cardData.title)}&cardSummary=${encodeURIComponent(cardData.summary)}`);
}; };
const handleReject = () => { const handleReject = () => {
removeCard(); removeCard();
}; };
// **** FIXED THIS FUNCTION ****
const handleReset = () => { const handleReset = () => {
setCards([]); // Reset to initial cards // This now resets the deck to the initial set of cards
setCards(initialCards);
}; };
return ( return (
<div <div
className="grid h-screen w-full place-items-center bg-orange-100" className="grid h-screen w-full place-items-center bg-orange-100"
// -gradient-to-tr from-orange-200 to-orange-100
style={{ style={{
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 32 32' width='32' height='32' fill='none' viewBox='0 0 32 32' width='32' height='32' fill='none'
stroke-width='2' stroke='%23d4d4d4'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e")`, stroke-width='2' stroke='%23d4d4d4'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e")`,
}} }}
> >
{cards.length > 0 ? ( {currentCards.length > 0 ? (
cards.map((card, index) => { currentCards.map((card, index) => {
const colorWay = COLOR_WAYS[card.id % COLOR_WAYS.length]; const colorWay = COLOR_WAYS[card.id % COLOR_WAYS.length];
return ( return (
<Card <Card
key={card.id} key={card.id}
cardData={card} cardData={card}
colorWay={colorWay} colorWay={colorWay}
isFront={index === cards.length - 1} isFront={index === currentCards.length - 1}
onAccept={handleAccept} onAccept={handleAccept}
onReject={handleReject} onReject={handleReject}
/> />
); );
}) })
) : ( ) : (
// ... (empty state)
<div className="text-center"> <div className="text-center">
<h2 className="text-2xl font-bold text-neutral-700"> <h2 className="text-2xl font-bold text-neutral-700">
No more cards! No more cards!
@@ -126,15 +206,12 @@ const Card: React.FC<CardProps> = ({ cardData, colorWay, isFront, onAccept, onRe
const handleDragEnd = async (event: MouseEvent | TouchEvent | PointerEvent, info: { offset: { x: number } }) => { const handleDragEnd = async (event: MouseEvent | TouchEvent | PointerEvent, info: { offset: { x: number } }) => {
if (info.offset.x > 100) { if (info.offset.x > 100) {
// Card accepted await controls.start({ x: 1000, opacity: 0, transition: { duration: 0.1 } });
await controls.start({ x: 1000, opacity: 0, transition: { duration: 0.1 } }); // Animate off-screen quickly onAccept(cardData);
onAccept(cardData); // Then call onAccept which removes the card and redirects
} else if (info.offset.x < -100) { } else if (info.offset.x < -100) {
// Card rejected await controls.start({ x: -1000, opacity: 0, transition: { duration: 0.1 } });
await controls.start({ x: -1000, opacity: 0, transition: { duration: 0.1 } }); // Animate off-screen quickly onReject();
onReject(); // Then call onReject which removes the card
} else { } else {
// Card not swiped far enough, snap back to center
controls.start({ x: 0, rotate: 0, opacity: 1, transition: { type: "spring", stiffness: 300, damping: 30 } }); controls.start({ x: 0, rotate: 0, opacity: 1, transition: { type: "spring", stiffness: 300, damping: 30 } });
} }
}; };
@@ -147,7 +224,7 @@ const Card: React.FC<CardProps> = ({ cardData, colorWay, isFront, onAccept, onRe
rotate, rotate,
opacity, opacity,
backgroundColor: colorWay.card, backgroundColor: colorWay.card,
color: colorWay.text, // This is the default text color color: colorWay.text,
zIndex: isFront ? 2 : 1, zIndex: isFront ? 2 : 1,
pointerEvents: isFront ? "auto" : "none", pointerEvents: isFront ? "auto" : "none",
}} }}
@@ -205,6 +282,4 @@ const Card: React.FC<CardProps> = ({ cardData, colorWay, isFront, onAccept, onRe
</motion.div> </motion.div>
); );
}; };
export default SwipeCards;
+112 -75
View File
@@ -5,22 +5,47 @@ import { useSearchParams } from "next/navigation";
import ChatInput from "../utils/component/ui/ChatInput"; import ChatInput from "../utils/component/ui/ChatInput";
import MessageWindow from "../utils/component/ui/MessageWindow"; import MessageWindow from "../utils/component/ui/MessageWindow";
import { ChatHistory, ChatSetting, Message, MessageRole } from "@/app/exportType/types"; import { ChatHistory, ChatSetting, Message, MessageRole } from "@/app/exportType/types";
import { getChatSessionByProjectId, saveChatSession } from "../utils/storage";
function getInitialChatState(
projectId: string | null,
): ChatHistory {
if (typeof window === 'undefined' || !projectId) {
return [];
}
const existingSession = getChatSessionByProjectId(projectId);
if (existingSession) {
return existingSession.history;
}
return [];
}
export default function Home() { export default function Home() {
const [history, setHistory] = useState<ChatHistory>([]) const searchParams = useSearchParams();
const historyRef = useRef<ChatHistory>([]); // Create a ref for history const projectId = searchParams.get('projectId');
const projectTitle = searchParams.get('projectTitle');
const cardTitle = searchParams.get('cardTitle');
const cardSummary = searchParams.get('cardSummary');
const [history, setHistory] = useState<ChatHistory>(() => getInitialChatState(projectId));
const [loading, setLoading] = useState(false);
const historyRef = useRef<ChatHistory>(history); // Create a ref for history
const [settings] = useState<ChatSetting>({ const [settings] = useState<ChatSetting>({
temperature: 1, temperature: 1,
model: "gemini-2.5-pro", model: "gemini-2.5-pro",
sysTemInstructions: "you are a ai helper. Your goal is to expand the project idea and interact with the user on any problem or question they have with the project. You are not allow to code the project for them and stray away from your job" systemInstructions: "you are a ai helper. Your goal is to expand the project idea and interact with the user on any problem or question they have with the project. You are not allow to code the project for them and stray away from your job"
}); });
const searchParams = useSearchParams();
const initialMessageSent = useRef(false); const initialMessageSent = useRef(false);
useEffect(() => { useEffect(() => {
historyRef.current = history; // Keep the ref updated with the latest history historyRef.current = history; // Keep the ref updated with the latest history
}, [history]); if (history.length > 0 && projectId && projectTitle) {
saveChatSession(projectId, projectTitle, history);
}
}, [history, projectId, projectTitle]);
const handleSend = useCallback(async (message: string) => { const handleSend = useCallback(async (message: string) => {
const newUserMessage: Message = { const newUserMessage: Message = {
@@ -28,24 +53,90 @@ export default function Home() {
parts: [{ text: message }], parts: [{ text: message }],
}; };
setHistory(prevHistory => { const currentHistory = [...historyRef.current, newUserMessage];
const currentHistory = [...prevHistory, newUserMessage]; setHistory(currentHistory);
setLoading(true);
const sendApiRequest = async () => { try {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userMessage: message,
history: historyRef.current, // Use the ref for history *before* the new user message
settings: settings,
}),
});
const data = await response.json();
setLoading(false);
if (data.error) {
console.error("AI Error:", data.error);
// Add an error message to chat
const aiErrorMessage: Message = {
role: "model",
parts: [{ text: `Error: ${data.error}` }],
};
setHistory(prevHistory => [...prevHistory, aiErrorMessage]);
return;
}
const aiMessage: Message = {
role: "model" as MessageRole,
parts: [{ text: data.response }],
};
// Set the final history with the AI response
// This will trigger the useEffect to save
setHistory(prevHistory => [...prevHistory, aiMessage]);
} catch (error) {
console.error("Request Failed:", error);
setLoading(false);
const aiErrorMessage: Message = {
role: "model",
parts: [{ text: `Error: Could not connect to AI service.` }],
};
setHistory(prevHistory => [...prevHistory, aiErrorMessage]);
}
}, [settings, projectId, projectTitle]); // Added projectId and projectTitle
// This effect handles the *very first* message when selecting a card
useEffect(() => {
// Only run if:
// 1. We have a cardTitle (meaning we came from the cards page)
// 2. The history is empty (it's a new chat)
// 3. We haven't already tried to send this (initialMessageSent ref)
if (cardTitle && cardSummary && history.length === 0 && !initialMessageSent.current) {
initialMessageSent.current = true; // Mark as "attempted"
setLoading(true);
const message = `Explain the project titled '${cardTitle}' with the summary '${cardSummary}' in detail, and suggest suitable roles for a team working on it.`;
const newUserMessage: Message = {
role: "user",
parts: [{ text: message }],
};
// Add user message to history
// The useEffect for 'history' will save this
setHistory([newUserMessage]);
const sendInitialApiRequest = async () => {
try { try {
const response = await fetch("/api/chat", { const response = await fetch("/api/chat", {
method: "POST", method: "POST",
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json",
},
body: JSON.stringify({ body: JSON.stringify({
userMessage: message, userMessage: message,
history: historyRef.current, // Use the ref for history history: [], // Send empty history for the first message
settings: settings, settings: settings,
}), }),
}); });
const data = await response.json(); const data = await response.json();
setLoading(false);
if (data.error) { if (data.error) {
console.error("AI Error:", data.error); console.error("AI Error:", data.error);
@@ -57,78 +148,24 @@ export default function Home() {
parts: [{ text: data.response }], parts: [{ text: data.response }],
}; };
setHistory(prevHistory => [...prevHistory, aiMessage]); // Add AI response
// The useEffect for 'history' will save this new state
} setHistory((prevHistory) => [...prevHistory, aiMessage]);
catch (error) { } catch (error) {
console.error("Request Failed:", error); console.error("Request Failed:", error);
setLoading(false);
} }
}; };
sendApiRequest();
return currentHistory; sendInitialApiRequest();
});
}, [settings]); // Now handleSend only depends on settings
useEffect(() => {
if (!initialMessageSent.current) {
const title = searchParams.get('title');
const summary = searchParams.get('summary');
if (title && summary) {
const message = `Explain the project titled '${title}' with the summary '${summary}' in detail, and suggest suitable roles for a team working on it.`;
const newUserMessage: Message = {
role: "user",
parts: [{ text: message }],
};
setTimeout(() => {
setHistory((prevHistory) => [...prevHistory, newUserMessage]); // Add user message to history
}, 0);
const sendInitialApiRequest = async () => {
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userMessage: message,
history: [...historyRef.current, newUserMessage], // Use ref for history
settings: settings,
}),
});
const data = await response.json();
if (data.error) {
console.error("AI Error:", data.error);
return;
}
const aiMessage: Message = {
role: "model" as MessageRole,
parts: [{ text: data.response }],
};
setHistory((prevHistory) => [...prevHistory, aiMessage]);
} catch (error) {
console.error("Request Failed:", error);
}
};
sendInitialApiRequest();
initialMessageSent.current = true;
}
} }
}, [searchParams, settings]); // Dependencies for this useEffect }, [cardTitle, cardSummary, history.length, settings, projectId, projectTitle]);
return ( return (
<div className="flex flex-col py-32"> <div className="flex flex-col py-32">
<MessageWindow history={history} /> {/* Pass loading state to MessageWindow */}
<MessageWindow history={history} isLoading={loading} />
<ChatInput onSend={handleSend} onOpenSettings={() => { }} /> <ChatInput onSend={handleSend} onOpenSettings={() => { }} />
</div> </div>
); );
+1 -4
View File
@@ -14,16 +14,13 @@ export interface GenerateConfig {
temperature: number; temperature: number;
topP: number; topP: number;
responseMimeType: string; responseMimeType: string;
<<<<<<< HEAD
maxOutputTokens?: number; maxOutputTokens?: number;
=======
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
} }
export interface ChatSetting { export interface ChatSetting {
temperature: number; temperature: number;
model: string; model: string;
sysTemInstructions: string; systemInstructions: string;
} }
export interface CardData { export interface CardData {
+36 -133
View File
@@ -1,10 +1,12 @@
"use client"; "use client";
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import FormPage from './FormPage'; import FormPage from './FormPage';
import { ChatSetting } from "../exportType/types"; import { ChatSetting } from "../exportType/types";
import { FormData, PROFICIENCY_LEVELS } from './formTypes'; import { FormData, PROFICIENCY_LEVELS } from './formTypes';
import { saveProject } from '../utils/storage';
import { useRouter } from "next/navigation";
// --- Helper function to get default form data ---
const getDefaultFormData = (): FormData => ({ const getDefaultFormData = (): FormData => ({
Hackathon: '', Hackathon: '',
mainTheme: '', mainTheme: '',
@@ -15,7 +17,6 @@ const getDefaultFormData = (): FormData => ({
}], }],
}); });
// --- New Initializer Function ---
const getInitialState = () => { const getInitialState = () => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return { return {
@@ -29,8 +30,8 @@ const getInitialState = () => {
localStorage.removeItem('formData'); localStorage.removeItem('formData');
return { return {
data: JSON.parse(storedFormData) as FormData, data: JSON.parse(storedFormData) as FormData,
showForm: false, showForm: true,
runEffect: true, runEffect: false,
}; };
} }
return { return {
@@ -40,58 +41,19 @@ const getInitialState = () => {
}; };
}; };
// FIX 1 (Memoization): Moved settings OUTSIDE the component.
// This makes it a true constant.
const AI_SETTINGS: ChatSetting = { const AI_SETTINGS: ChatSetting = {
temperature: 1, temperature: 1,
model: "gemini-2.5-pro", model: "gemini-2.5-pro",
sysTemInstructions: `You are an elite hackathon idea generator. Your task is to generate five different creative, feasible, and relevant project idea. systemInstructions: `You are an elite hackathon idea generator. Your task is to generate five different creative, feasible, and relevant project idea.
The output MUST be in Markdown format and strictly follow this structure: The output MUST be in Markdown format and strictly follow this structure:
### Project Idea Title ### Project Idea Title
<<<<<<< HEAD **Relevance to Theme:** [Explain briefly]
**Track** [print out the track you used] **Core Technology:** [List 3-5 primary technologies]
**Concept:** [A concise, single paragraph description of the product and its primary function.]`, **Team Fit:** [How the team's skills apply]
}); **Concept:** [A concise, single paragraph description of the product and its primary function.]
**Bonus Feature:** [A single, ambitious feature to impress judges.]`
const parseAiResponse = (markdownString: string): CardData[] => {
console.log("Raw AI Response:", markdownString);
const ideas: CardData[] = [];
const ideaBlocks = markdownString.split("### ").filter(block => block.trim() !== "");
ideaBlocks.forEach((block, index) => {
// Match title: anything from the start of the block until the first newline or the start of "**Track**"
const titleMatch = block.match(/^(.*?)(?=\n\*\*Track\*\*|$)/s);
// Match track: "**Track**" followed by optional whitespace and then capture everything until the next "**Concept**" or end of block/line
const trackMatch = block.match(/\*\*Track\*\*\s*:\s*(.*?)(?=\n\*\*Concept\*\*|$)/s); // Added ':' and made whitespace more flexible
// Match concept: "**Concept:**" followed by optional whitespace and then capture everything until the end of the block
const conceptMatch = block.match(/\*\*Concept:\*\*\s*(.*)/s);
const title = titleMatch ? titleMatch[1].trim() : `Idea ${index + 1}`;
const summary = conceptMatch ? conceptMatch[1].trim() : "No concept provided.";
const tracksRaw = trackMatch ? trackMatch[1].trim() : "";
console.log("tracksRaw:", tracksRaw); // Added console.log for debugging
const tracks = tracksRaw.split(',').map(track => track.trim()).filter(track => track !== '');
ideas.push({
id: index + 1,
title,
tracks,
summary,
});
});
console.log("Parsed Cards:", ideas);
return ideas;
};
=======
**Relevance to Theme:** [Explain briefly]
**Core Technology:** [List 3-5 primary technologies]
**Team Fit:** [How the team's skills apply]
**Concept:** [A concise, single paragraph description of the product and its primary function.]
**Bonus Feature:** [A single, ambitious feature to impress judges.]`,
}; };
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
// ------------------------------------------------
export default function Home() { export default function Home() {
@@ -100,17 +62,19 @@ export default function Home() {
const [formData, setFormData] = useState<FormData>(initialState.data); const [formData, setFormData] = useState<FormData>(initialState.data);
const [ideaResponse, setIdeaResponse] = useState<string>(""); const [ideaResponse, setIdeaResponse] = useState<string>("");
// The 'settings' state [useState] is now removed from here. const router = useRouter();
const handleFormSubmitFromFormPage = (submittedData: FormData) => { const handleFormSubmitFromFormPage = (submittedData: FormData) => {
const savedProject = saveProject(submittedData);
if (typeof window !== 'undefined') localStorage.removeItem('formData');
setFormData(submittedData); setFormData(submittedData);
setShowForm(false); setShowForm(false);
generateIdeas(submittedData); generateIdeas(submittedData, savedProject.id);
}; };
// Wrapped generateIdeas in useCallback const generateIdeas = useCallback(async (dataToProcess: FormData, projectId: string) => {
const generateIdeas = useCallback(async (dataToProcess: FormData) => {
// 1. Data Cleaning
const submissionData: FormData = { const submissionData: FormData = {
...dataToProcess, ...dataToProcess,
tracksStack: dataToProcess.tracksStack.filter(tracks => tracks.trim() !== ''), tracksStack: dataToProcess.tracksStack.filter(tracks => tracks.trim() !== ''),
@@ -122,7 +86,6 @@ export default function Home() {
.filter(student => student.studentName.trim() !== '' || student.skills.length > 0), .filter(student => student.studentName.trim() !== '' || student.skills.length > 0),
}; };
// 2. Prompt Construction
const prompt = ` const prompt = `
Hackathon: ${submissionData.Hackathon} Hackathon: ${submissionData.Hackathon}
Main Theme: ${submissionData.mainTheme} Main Theme: ${submissionData.mainTheme}
@@ -136,71 +99,29 @@ export default function Home() {
// 3. API Call // 3. API Call
try { try {
setIdeaResponse("Generating ideas... Please wait."); setIdeaResponse("Generating ideas... Please wait.");
<<<<<<< HEAD
setGeneratedCards([]); // Clear previous cards
const response = await fetch("/api/generate-idea", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: prompt,
settings: settings,
}),
});
const data = await response.json();
=======
const response = await fetch("/api/generate-idea", { const response = await fetch("/api/generate-idea", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
prompt: prompt, prompt: prompt,
settings: AI_SETTINGS, // Use the constant settings: AI_SETTINGS,
}), }),
}); });
const data = await response.json(); const data = await response.json();
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
if (data.error || !response.ok) { if (data.error || !response.ok) {
console.error("AI Error:", data.error); console.error("AI Error:", data.error);
setIdeaResponse(`Error: Failed to generate idea. ${data.error || response.statusText}`); setIdeaResponse(`Error: Failed to generate idea. ${data.error || response.statusText}`);
return; return;
<<<<<<< HEAD
}
setIdeaResponse(data.response);
const parsedCards = parseAiResponse(data.response);
setGeneratedCards(parsedCards);
} catch (error) {
console.error("Request Failed:", error);
let errorMessage = "An unknown error occurred.";
if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === 'string') {
errorMessage = error;
}
setIdeaResponse(`A network error occurred. Please try again. Details: ${errorMessage}`);
}
}, [setIdeaResponse, setGeneratedCards, settings, parseAiResponse]);
useEffect(() => {
if (typeof window !== 'undefined') {
const storedFormData = localStorage.getItem('formData');
if (storedFormData) {
const parsedData: FormData = JSON.parse(storedFormData);
setTimeout(() => setShowForm(false), 0);
generateIdeas(parsedData);
localStorage.removeItem('formData');
=======
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
} }
setIdeaResponse(data.response); setIdeaResponse(data.response);
// FIX 2 (Unexpected any): Changed 'any' to 'unknown' and added a type check. if (typeof window !== 'undefined') localStorage.setItem('temp_aiResponse', data.response);
router.push(`/cards?projectId=${projectId}&projectTitle=${encodeURIComponent(dataToProcess.Hackathon)}`);
} catch (error: unknown) { } catch (error: unknown) {
console.error("Request Failed:", error); console.error("Request Failed:", error);
let errorMessage = "A network error occurred. Please try again."; let errorMessage = "A network error occurred. Please try again.";
@@ -209,15 +130,12 @@ export default function Home() {
} }
setIdeaResponse(errorMessage); setIdeaResponse(errorMessage);
} }
// FIX 3 (Linter): Disable the faulty memoization rule. }, [router]);
// The empty array is correct because setIdeaResponse is a stable state setter.
// eslint-disable-next-line react-hooks/preserve-manual-memoization
}, []);
// Replaced useEffect // Replaced useEffect
useEffect(() => { useEffect(() => {
if (initialState.runEffect) { if (initialState.runEffect) {
generateIdeas(initialState.data); //generateIdeas(initialState.data, "some-resumed-id");
} }
}, [initialState, generateIdeas]); }, [initialState, generateIdeas]);
@@ -229,25 +147,6 @@ export default function Home() {
<FormPage onFormSubmit={handleFormSubmitFromFormPage} /> <FormPage onFormSubmit={handleFormSubmitFromFormPage} />
) : ( ) : (
<> <>
<<<<<<< HEAD
{generatedCards.length > 0 ? (
<SwipeCards initialCards={generatedCards} />
) : (
<>
<h1 className="text-3xl font-extrabold text-center text-gray-800 mb-8 tracking-tight">
Generated Project Idea 💡
</h1>
<div className="mt-8 p-4 bg-yellow-50 rounded-lg border border-yellow-200">
<h2 className="text-xl font-bold text-gray-700 mb-2">Generated Ideas:</h2>
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: ideaResponse.replace(/\n/g, '<br/>') }}
/>
{ideaResponse === "" && <p className="text-gray-500">Submit the form to generate project ideas.</p>}
</div>
</>
)}
=======
<h1 className="text-3xl font-extrabold text-center text-gray-800 mb-8 tracking-tight"> <h1 className="text-3xl font-extrabold text-center text-gray-800 mb-8 tracking-tight">
Generated Project Idea 💡 Generated Project Idea 💡
</h1> </h1>
@@ -259,12 +158,16 @@ export default function Home() {
/> />
{ideaResponse === "" && <p className="text-gray-500">Submit the form to generate project ideas.</p>} {ideaResponse === "" && <p className="text-gray-500">Submit the form to generate project ideas.</p>}
</div> </div>
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
<button <button
onClick={() => setShowForm(true)} onClick={() => {
setShowForm(true);
if(typeof window !== 'undefined') localStorage.removeItem('formData');
setFormData(getDefaultFormData());
}
}
className="mt-6 w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-lg text-lg font-semibold text-white bg-blue-600 hover:bg-blue-700 transition duration-150" className="mt-6 w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-lg text-lg font-semibold text-white bg-blue-600 hover:bg-blue-700 transition duration-150"
> >
Go Back to Form Create New Project
</button> </button>
</> </>
)} )}
+1 -56
View File
@@ -1,35 +1,3 @@
<<<<<<< HEAD
=======
/* @import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
:root {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
} */
/* 1. Import BOTH fonts in one go */
@import url('https://fonts.googleapis.com/css2?family=Concert+One&family=Pixelify+Sans:wght@400;700&display=swap');
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
@@ -40,22 +8,8 @@ body {
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
/* 3. Assign fonts to Tailwind's theme */
<<<<<<< HEAD
/* Tell Tailwind to use the variables already defined in app/layout.tsx */
--font-sans: var(--font-sans);
--font-header: var(--font-header);
=======
/* --font-sans is for regular text (Concert One) */
--font-sans: var(--font-concert-one); --font-sans: var(--font-concert-one);
/* We add a new font family for headers (Pixelify Sans) */
/* This also creates a 'font-header' utility class */
--font-header: var(--font-pixelify); --font-header: var(--font-pixelify);
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
} }
@@ -69,17 +23,8 @@ body {
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
/* 4. Body uses --font-sans (Concert One) by default */
font-family: var(--font-sans); font-family: var(--font-sans);
} }
h1,h2,h3,h4,h5,h6 {
/* 5. (Optional but helpful) Apply header font to h-tags by default */
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-header); font-family: var(--font-header);
} }
+8 -273
View File
@@ -1,4 +1,5 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
export default function Home() { export default function Home() {
return ( return (
@@ -16,135 +17,12 @@ export default function Home() {
priority priority
/> />
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<a href="/signin" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors"> <Link href="/Dashboard" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Sign in Dashboard
</a> </Link>
<a href="/register" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Register
</a>
</div> </div>
</header> </header>
<<<<<<< HEAD
export default function Page() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<header className="absolute top-6 left-8 right-8 flex items-center justify-between text-sm font-medium text-zinc-700 dark:text-zinc-300">
<Image
className=""
src="/logo.svg"
alt="logo"
width={100}
height={20}
priority
/>
<div className="flex items-center gap-4">
<Link href="/auth/login" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Sign in
</Link>
<Link href="/auth/logout" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Log out
</Link>
</div>
</header>
<main className="flex min-h-screen w-full flex-col justify-between py-32 px-16 bg-white dark:bg-black">
<div className="flex flex-col sm:flex-row items-start justify-between gap-12 text-left w-full">
<div className="flex-1 flex flex-col gap-5 justify-center">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
(project name)
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<Link
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</Link>{" "}
or the{" "}
<Link
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</Link>{" "}
center.
</p>
<ProtectedNavButton>Get Started</ProtectedNavButton>
</div>
<div className="flex-1 flex justify-center">
<div className="w-full max-w-[400px] h-[300px] bg-zinc-200 dark:bg-zinc-800 rounded-xl shadow-md flex items-center justify-center text-zinc-500">
Image Placeholder
</div>
</div>
</div>
<section className="mt-32 w-full flex flex-col items-start gap-12">
<div>
<h2 className="text-[28px] font-semibold text-black dark:text-zinc-50 leading-9">
Guide:
</h2>
</div>
<div className="flex flex-col sm:flex-row items-start justify-between gap-8 w-full">
<div className="w-full max-w-xl">
<h2 className="text-2xl font-semibold text-black dark:text-zinc-50 mb-2">
First Text Block
</h2>
<p className="text-lg text-zinc-600 dark:text-zinc-400">
This is the content for the first text block. You can write anything here.
</p>
</div>
<div className="sm:w-1/2 flex justify-center items-center">
<div className="w-full max-w-[300px] h-[200px] bg-zinc-200 dark:bg-zinc-800 rounded-xl shadow-md flex items-center justify-center text-zinc-500">
Image Placeholder
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row items-start justify-between gap-8 w-full">
<div className="sm:w-1/2 flex justify-center items-center">
<div className="w-full max-w-[300px] h-[200px] bg-zinc-200 dark:bg-zinc-800 rounded-xl shadow-md flex items-center justify-center text-zinc-500">
Image Placeholder
</div>
</div>
<div className="flex w-full justify-end">
<div className="w-full max-w-xl">
<h2 className="text-2xl font-semibold text-black dark:text-zinc-50 mb-2">
Second Text Block
</h2>
<p className="text-lg text-zinc-600 dark:text-zinc-400">
This is the content for the second text block. You can write anything here.
</p>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row items-start justify-between gap-8 w-full">
<div className="w-full max-w-xl">
<h2 className="text-2xl font-semibold text-black dark:text-zinc-50 mb-2">
Third Text Block
</h2>
<p className="text-lg text-zinc-600 dark:text-zinc-400">
This is the content for the third text block. You can write anything here.
</p>
</div>
<div className="sm:w-1/2 flex justify-center items-center">
<div className="w-full max-w-[300px] h-[200px] bg-zinc-200 dark:bg-zinc-800 rounded-xl shadow-md flex items-center justify-center text-zinc-500">
Image Placeholder
</div>
</div>
</div>
</section>
</main>
</div>
);
=======
<main className="flex min-h-screen w-full flex-col justify-between py-32 max-w-[1200px] mx-auto"> <main className="flex min-h-screen w-full flex-col justify-between py-32 max-w-[1200px] mx-auto">
<div className="flex flex-col sm:flex-row items-center justify-center gap-12 text-left w-full max-w-[1200px] mx-auto"> <div className="flex flex-col sm:flex-row items-center justify-center gap-12 text-left w-full max-w-[1200px] mx-auto">
<div className="flex-1 flex flex-col gap-5 justify-center items-start"> <div className="flex-1 flex flex-col gap-5 justify-center items-start">
@@ -159,30 +37,15 @@ export default function Page() {
className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400" className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"
> >
Got a hackathon coming up? Tell us your track, your team’s skills, what you’re passionate about, and more — and we’ll provide you with the perfect project idea!{" "} Got a hackathon coming up? Tell us your track, your team’s skills, what you’re passionate about, and more — and we’ll provide you with the perfect project idea!{" "}
{/* <a
                href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
                className="font-medium text-zinc-950 dark:text-zinc-50"
              >
                Templates
              </a>{" "}
              or the{" "}
              <a
                href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
                className="font-medium text-zinc-950 dark:text-zinc-50"
              >
                Learning
              </a>{" "} */}
{/* center. */}
</p> </p>
{/* <ProtectedNavButton>Get Started</ProtectedNavButton> */} <Link
<button href="/Dashboard"
style={{ backgroundColor: "#173C46", color: "white" }} style={{ backgroundColor: "#173C46", color: "white" }}
className="px-4 py-1.5 rounded-md shadow-md font-semibold hover:opacity-90 transition-opacity" className="px-4 py-1.5 rounded-md shadow-md font-semibold hover:opacity-90 transition-opacity"
> >
{/* {children} */}
Get Started Get Started
</button> </Link>
</div> </div>
<div className="flex-1 flex justify-center"> <div className="flex-1 flex justify-center">
@@ -279,132 +142,4 @@ export default function Page() {
</main> </main>
</div> </div>
); );
>>>>>>> 44b5a4dc49ea757e9790ba8309edc6b37d764105 }
}
// {/* <div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
//           <a
//             className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
//             href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
//             target="_blank"
//             rel="noopener noreferrer"
//           >
//             <Image
//               className="dark:invert"
//               src="/vercel.svg"
//               alt="Vercel logomark"
//               width={16}
//               height={16}
//             />
//             Deploy Now
//           </a>
//           <a
//             className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
//             href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
//             target="_blank"
//             rel="noopener noreferrer"
//           >
//             Documentation
//           </a>
//         </div> */}
// "use client";
// import React, { use, useState } from "react";
// import { useRouter } from "next/navigation";
// export default function Dashboard() {
//   const router = useRouter();  
//   const[query, setQuery] = useState("");
//   // const [projects, setProjects] = useState( [
//   //     { id: 1, name: "Alpha Project", createdAt: new Date("2025-01-01")},
//   //     { id: 2, name: "Beta Board", createdAt: new Date("2025-02-15")},
//   //     { id: 3, name: "Gamma Task", createdAt: new Date("2025-03-10")}
//   // ]);
//   // const [newProjectName, setNewProjectName] = useState("");
//   // const filtered = projects.filter( (p) => p.name.toLowerCase().includes(query.toLowerCase()) );
//   // const handleCreate = () => {
//   //   if(!newProjectName.trim()) return;
//   //   const newProject = {
//   //     id: projects.length+1, name: newProjectName, createdAt: new Date(),
//   //   };
//   // };
//   const projects = [
//     {name: "Alpha Project", createdAt: "2025-01-01"},
//     {name: "Beta Board", createdAt: "2025-02-15"},
//     {name: "Gamma Tasl", createdAt: "2025-03-10"},
//   ]
//   const filtered = projects.filter(
//     (p) => p.name.toLowerCase().includes(query.toLowerCase())
//   );
//  return (
//    <div className="min-h-screen bg-zinc-50 dark:bg-black font-sans">
//      <header className="sticky top-0 z-10 bg-white dark:bg-black/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-800">
//        <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between gap-4">
//          <div className="flex-1">
//            {/* <label htmlFor="dashboard-search" className="sr-only">
//              Search
//            </label> */}
//            <input
//              id="dashboard-search"
//              value={query}
//              onChange={(e) => setQuery(e.target.value)}
//              placeholder="Search projects, boards or tasks..."
//              className="w-full max-w-md px-4 py-2 rounded-md border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
//            />
//          </div>
//          <div className="flex items-center gap-2">
//            {/* <input
//               type = "text"
//               value = {newProjectName}
//               onChange = { (e) => setNewProjectName(e.target.value)}
//               placeholder="New Project Name"
//               className="px-3 py-2 rounded-md border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900  text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
//             /> */}
//             <button
//              type="button"
//              // className="ml-4 inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md shadow-sm"
//              // onClick={() => alert("Create clicked — wire up creation flow here.")}
//               onClick={ () => router.push("/dashbaord/create")}
//               className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md shadow-sm"
//            >
//              Create
//            </button>
//          </div>
//        </div>
//      </header>
//      <main className="max-w-7xl mx-auto px-6 py-10">
//        <h1 className="text-2xl font-semibold text-black dark:text-zinc-50 mb-4">
//          Project Names
//        </h1>
//        <section>
//          <ul className="space-y-3">
//            {filtered.map((project, index) => (
//              <li
//                key={index}
//                // className="p-4 bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 rounded-md text-zinc-800 dark:text-zinc-100"
//                 className="flex justify-between p-4 bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 rounded-md text-zinc-800 dark:text-zinc-100"
//               >
//                <span>{project.name}</span>
//                 <span className="text-zinc-500 dark:text-zinc-400 text-sm">
//                   {/* {project.createdAt.toLocaleDateString()} */}
//                   Created: {project.createdAt}
//                 </span>
//              </li>
//            ))}
//            {filtered.length === 0 && (
//              <li className="text-zinc-500">No results found</li>
//            )}
//          </ul>
//        </section>
//      </main>
//    </div>
//  );
// }
+19 -3
View File
@@ -4,15 +4,16 @@ import { ChatHistory } from "@/app/exportType/types";
import { User, Bot } from "lucide-react" import { User, Bot } from "lucide-react"
interface MessageWindowProps { interface MessageWindowProps {
history: ChatHistory history: ChatHistory;
isLoading: boolean;
} }
export default function MessageWindow({ history }: MessageWindowProps) { export default function MessageWindow({ history, isLoading }: MessageWindowProps) {
const messageEndRef = useRef<HTMLDivElement>(null); const messageEndRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
messageEndRef.current?.scrollIntoView({ behavior: "smooth" }); messageEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [history]); }, [history, isLoading]);
return ( return (
<div className="flex-1 p-3 overflow-y-auto"> <div className="flex-1 p-3 overflow-y-auto">
@@ -51,6 +52,21 @@ export default function MessageWindow({ history }: MessageWindowProps) {
</div> </div>
); );
})} })}
{isLoading && (
<div className="flex justify-start mb-4">
<div className="mr-2">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200">
<Bot size={15} className="text-gray-700" />
</div>
</div>
<div className="px-4 py-2 shadow-sm rounded-lg bg-white text-gray-800 rounded-bl-none max-w-xs sm:max-w-md">
<div className="whitespace-pre-wrap break-words">
{/* Simple loading dots animation */}
<span className="animate-pulse">...</span>
</div>
</div>
</div>
)}
{/* Invisible element to help scroll to bottom */} {/* Invisible element to help scroll to bottom */}
<div ref={messageEndRef} /> <div ref={messageEndRef} />
+3 -3
View File
@@ -16,7 +16,7 @@ export async function chatToGemini(
): Promise<string> { ): Promise<string> {
const model = genAI.getGenerativeModel({ const model = genAI.getGenerativeModel({
model: settings.model || "gemini-2.5-flash", model: settings.model || "gemini-2.5-flash",
systemInstruction: settings.sysTemInstructions || "you are going to create a project idea, explain it in summery first before going to detail", systemInstruction: settings.systemInstructions || "you are going to create a project idea, explain it in summery first before going to detail",
}); });
const generationConfig: GenerateConfig = { const generationConfig: GenerateConfig = {
@@ -36,7 +36,7 @@ export async function chatToGemini(
return result.response.text(); return result.response.text();
} }
catch (error) { catch (error) {
console.error(error) console.error("Gemini API Error:", error);
throw error throw error;
} }
} }
+94
View File
@@ -0,0 +1,94 @@
import { FormData } from "@/app/formfile/formTypes";
import { ChatHistory } from "@/app/exportType/types";
// 1. Define the data structures for storage
export interface StoredProject extends FormData {
id: string;
title: string;
}
export interface StoredChatSession {
id: string;
projectId: string;
projectTitle: string;
history: ChatHistory;
}
export interface UserData {
projects: StoredProject[];
chatSessions: StoredChatSession[];
}
const STORAGE_KEY = "hackSparkUserData";
function safeGetLocalStorage(): UserData {
if (typeof window === 'undefined') {
return { projects: [], chatSessions: [] };
}
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : { projects: [], chatSessions: [] };
} catch (error) {
console.error("Failed to parse localStorage data:", error);
return { projects: [], chatSessions: [] };
}
}
//Retrieves all stored projects.
export function getStoredProjects(): StoredProject[] {
return safeGetLocalStorage().projects;
}
//Retrieves all stored chat sessions.
export function getStoredChatSessions(): StoredChatSession[] {
return safeGetLocalStorage().chatSessions;
}
//Finds a specific chat session by its project ID.
export function getChatSessionByProjectId(projectId: string): StoredChatSession | undefined {
if (typeof window === 'undefined') return undefined;
const data = safeGetLocalStorage();
return data.chatSessions.find(session => session.projectId === projectId);
}
export function saveProject(formData: FormData): StoredProject {
if (typeof window === 'undefined') throw new Error("localStorage is not available.");
const data = safeGetLocalStorage();
const newProject: StoredProject = {
...formData,
id: new Date().toISOString(),
title: formData.Hackathon || "Untitled Project",
};
data.projects.push(newProject);
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
return newProject;
}
export function saveChatSession(projectId: string, projectTitle: string, history: ChatHistory) {
if (typeof window === 'undefined') return;
const data = safeGetLocalStorage();
const existingSessionIndex = data.chatSessions.findIndex(session => session.projectId === projectId);
if (existingSessionIndex > -1) {
data.chatSessions[existingSessionIndex].history = history;
} else {
const newSession: StoredChatSession = {
id: new Date().toISOString(),
projectId,
projectTitle,
history,
};
data.chatSessions.push(newSession);
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
export function setProjectForEditing(project: StoredProject) {
if (typeof window === 'undefined') return;
localStorage.setItem('formData', JSON.stringify(project));
}