Skip to content

Commit dae5e2c

Browse files
committed
feat: Introduce new join page with team member display, futuristic hero, glowing AI chat assistant UI, and assistant API endpoint.
1 parent c89a03b commit dae5e2c

6 files changed

Lines changed: 158 additions & 181 deletions

File tree

app/api/assistant/route.ts

Lines changed: 76 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ You are the official AI assistant for Bits&Bytes, a teen-led code club based in
3333
- **Mission:** Innovation, collaboration, and real-world impact through technology.
3434
- **Activities:** Hackathons (e.g., Scrapyard Lucknow), workshops, and student mentorship.
3535
- **Contact:** hello@gobitsnbytes.org
36+
- **GitHub:** https://github.com/gobitsnbytes
3637
3738
**How to get answers:**
3839
1. **For Team/Roles:** DO NOT guess. Always use the 'find_team_expert' or 'recommend_role' tools. The team structure is dynamic.
@@ -51,6 +52,8 @@ You are the official AI assistant for Bits&Bytes, a teen-led code club based in
5152
5253
Rules:
5354
- Always stay truthful to Bits&Bytes.
55+
- Be extremely concise, conversational, and direct. Avoid long, multi-paragraph summaries or filler text. Get straight to the point.
56+
- Do not use 'suggest_navigation' and 'highlight_text' in the exact same response. If you navigate the user to a new page, wait for them to see it; do not highlight right away since the page will be loading.
5457
- If you can't find the answer in the tools or page content, admit it:
5558
"I’m not sure about that based on the information publicly available on this site."
5659
`
@@ -309,104 +312,95 @@ export async function POST(req: NextRequest) {
309312
messages,
310313
tools,
311314
tool_choice: "auto",
312-
max_completion_tokens: 400,
315+
max_tokens: 400,
313316
})
314317
}
315318

316319
let modelUsed = PRIMARY_MODEL
317-
let completion
320+
let currentMessages = [...baseMessages]
321+
let actionToClient: AssistantAction | undefined
318322

319-
try {
320-
completion = await runCompletion(PRIMARY_MODEL, baseMessages)
321-
} catch (err) {
322-
const apiError = err as APIError
323-
const code = (apiError as any)?.code ?? (apiError as any)?.error?.code
324-
const status = (apiError as any)?.status
325-
const shouldFallback =
326-
code === "model_not_found" ||
327-
code === "unsupported_parameter" ||
328-
code === "unsupported_value" ||
329-
status === 403
330-
331-
if (shouldFallback) {
332-
modelUsed = FALLBACK_MODEL
333-
completion = await runCompletion(FALLBACK_MODEL, baseMessages)
334-
} else {
335-
throw err
323+
for (let i = 0; i < 5; i++) {
324+
let completion
325+
try {
326+
completion = await runCompletion(PRIMARY_MODEL, currentMessages)
327+
} catch (err) {
328+
const apiError = err as APIError
329+
const code = (apiError as any)?.code ?? (apiError as any)?.error?.code
330+
const status = (apiError as any)?.status
331+
const shouldFallback =
332+
code === "model_not_found" ||
333+
code === "unsupported_parameter" ||
334+
code === "unsupported_value" ||
335+
status === 403
336+
337+
if (shouldFallback) {
338+
modelUsed = FALLBACK_MODEL
339+
completion = await runCompletion(FALLBACK_MODEL, currentMessages)
340+
} else {
341+
throw err
342+
}
336343
}
337-
}
338344

339-
const choice = completion.choices[0]
340-
const message = choice?.message
345+
const choice = completion.choices[0]
346+
const message = choice?.message
341347

342-
// If no tool calls, stream the final answer directly.
343-
if (!message?.tool_calls || message.tool_calls.length === 0) {
344-
try {
345-
return await streamAssistantResponse(modelUsed, baseMessages)
346-
} catch (streamErr) {
347-
console.error("Assistant stream error:", streamErr)
348-
const answer = message?.content?.trim()
349-
return NextResponse.json({
350-
answer: answer ?? "I’m not sure about that based on the information publicly available on this site.",
351-
})
348+
if (!message?.tool_calls || message.tool_calls.length === 0) {
349+
break // No more tool calls required
352350
}
353-
}
354351

355-
// Handle first tool call for now (this already gives agentic behaviour).
356-
const toolCall = message.tool_calls[0]
357-
const toolName = toolCall.function.name
358-
let toolArgs: any = {}
352+
currentMessages.push(message)
359353

360-
try {
361-
toolArgs = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {}
362-
} catch {
363-
toolArgs = {}
364-
}
354+
for (const toolCall of message.tool_calls) {
355+
const toolName = toolCall.function.name
356+
let toolArgs: any = {}
357+
try {
358+
toolArgs = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {}
359+
} catch {
360+
toolArgs = {}
361+
}
365362

366-
let toolResult: any = null
367-
let action: AssistantAction | undefined
368-
369-
if (toolName === "submit_contact_form") {
370-
toolResult = await handleSubmitContactTool(toolArgs)
371-
} else if (toolName === "suggest_navigation") {
372-
const path = normalizePath(toolArgs?.path)
373-
toolResult = { success: true, path }
374-
action = { type: "navigate" as const, path }
375-
} else if (toolName === "get_site_section") {
376-
toolResult = await handleGetSiteSectionTool(toolArgs?.section ?? "home", req)
377-
} else if (toolName === "find_team_expert") {
378-
const query = (toolArgs?.query ?? "").toString()
379-
const experts = findExperts(query)
380-
toolResult = { query, experts }
381-
} else if (toolName === "recommend_role") {
382-
const skills = Array.isArray(toolArgs?.skills) ? toolArgs.skills : []
383-
const interests = Array.isArray(toolArgs?.interests) ? toolArgs.interests : []
384-
const recommendation = recommendRoles(skills, interests)
385-
toolResult = { skills, interests, recommendation }
386-
} else if (toolName === "highlight_text") {
387-
const textSnippet = (toolArgs?.textSnippet ?? "").toString()
388-
toolResult = { success: true, textSnippet }
389-
action = { type: "highlight" as const, textSnippet }
390-
} else {
391-
toolResult = { success: false, message: `Unknown tool: ${toolName}` }
392-
}
363+
let toolResult: any = null
364+
365+
if (toolName === "submit_contact_form") {
366+
toolResult = await handleSubmitContactTool(toolArgs)
367+
} else if (toolName === "suggest_navigation") {
368+
const path = normalizePath(toolArgs?.path)
369+
toolResult = { success: true, path }
370+
actionToClient = { type: "navigate" as const, path }
371+
} else if (toolName === "get_site_section") {
372+
toolResult = await handleGetSiteSectionTool(toolArgs?.section ?? "home", req)
373+
} else if (toolName === "find_team_expert") {
374+
const query = (toolArgs?.query ?? "").toString()
375+
const experts = findExperts(query)
376+
toolResult = { query, experts }
377+
} else if (toolName === "recommend_role") {
378+
const skills = Array.isArray(toolArgs?.skills) ? toolArgs.skills : []
379+
const interests = Array.isArray(toolArgs?.interests) ? toolArgs.interests : []
380+
const recommendation = recommendRoles(skills, interests)
381+
toolResult = { skills, interests, recommendation }
382+
} else if (toolName === "highlight_text") {
383+
const textSnippet = (toolArgs?.textSnippet ?? "").toString()
384+
toolResult = { success: true, textSnippet }
385+
actionToClient = { type: "highlight" as const, textSnippet }
386+
} else {
387+
toolResult = { success: false, message: `Unknown tool: ${toolName}` }
388+
}
393389

394-
const messagesWithTool: OpenAI.Chat.ChatCompletionMessageParam[] = [
395-
...baseMessages,
396-
message,
397-
{
398-
role: "tool",
399-
tool_call_id: toolCall.id,
400-
content: JSON.stringify(toolResult),
401-
} as OpenAI.Chat.ChatCompletionToolMessageParam,
402-
]
390+
currentMessages.push({
391+
role: "tool",
392+
tool_call_id: toolCall.id,
393+
content: JSON.stringify(toolResult),
394+
} as OpenAI.Chat.ChatCompletionToolMessageParam)
395+
}
396+
}
403397

404398
try {
405-
return await streamAssistantResponse(modelUsed, messagesWithTool, action)
399+
return await streamAssistantResponse(modelUsed, currentMessages, actionToClient)
406400
} catch (streamErr) {
407401
console.error("Assistant stream error after tool call:", streamErr)
408402
return NextResponse.json(
409-
{ error: "Failed to stream the assistant response after tool call." },
403+
{ error: "Failed to stream the assistant response." },
410404
{ status: 500 }
411405
)
412406
}
@@ -427,7 +421,7 @@ async function streamAssistantResponse(
427421
const completion = await openai.chat.completions.create({
428422
model,
429423
messages,
430-
max_completion_tokens: 400,
424+
max_tokens: 400,
431425
stream: true,
432426
})
433427

@@ -444,6 +438,7 @@ async function streamAssistantResponse(
444438
try {
445439
for await (const part of completion) {
446440
const delta = part.choices[0]?.delta
441+
447442
if (delta?.content) {
448443
send({ type: "token", content: delta.content })
449444
}

app/join/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export default function Join() {
122122
<PageSection align="center">
123123
<div className="mx-auto w-full max-w-3xl space-y-6 sm:space-y-8">
124124
<GlassContainer className="p-8 md:p-12 text-center" glowColor="both">
125-
<div className="space-y-6">
125+
<div className="flex flex-col items-center gap-6">
126126
<div className="inline-flex items-center gap-2 text-xs sm:text-sm text-white/60 font-bold uppercase tracking-widest">
127127
<Clock className="h-4 w-4 text-(--brand-pink)" />
128128
<span>Takes less than 2 minutes</span>
@@ -135,14 +135,14 @@ export default function Join() {
135135
data-tally-align-left="1"
136136
data-tally-hide-title="1"
137137
data-tally-overlay="1"
138-
className="group rounded-full bg-(--brand-pink) px-12 py-8 text-lg md:text-xl font-black text-white shadow-[0_0_30px_rgba(228,90,146,0.5)] transition-all hover:scale-[1.03] hover:shadow-[0_0_60px_rgba(228,90,146,0.7)] w-full sm:w-auto"
138+
className="group inline-flex items-center justify-center gap-2 rounded-full bg-(--brand-pink) px-10 py-4 text-lg md:text-xl font-black text-white shadow-[0_0_30px_rgba(228,90,146,0.5)] transition-all hover:scale-[1.03] hover:shadow-[0_0_60px_rgba(228,90,146,0.7)] w-full sm:w-auto max-w-xs"
139139
>
140140
Apply to Join
141-
<ArrowRight className="ml-2 h-6 w-6 transition-transform group-hover:translate-x-1" />
141+
<ArrowRight className="h-6 w-6 shrink-0 transition-transform group-hover:translate-x-1" />
142142
</Button>
143143

144144
<p className="text-sm text-white/50 font-medium">
145-
We review applications weekly · You'll hear back within 7 days
145+
We review applications weekly · You&apos;ll hear back within 7 days
146146
</p>
147147
</div>
148148
</GlassContainer>

components/team-case-study.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ function TeamCard({
108108
<div
109109
className={cn(
110110
"relative flex cursor-pointer flex-col rounded-xl sm:rounded-2xl p-3 sm:p-4 transition-all duration-700",
111-
// Fixed card height for consistency across all cards
112-
"h-[460px] sm:h-[500px] md:h-[540px]",
111+
// Use h-full so CSS grid can ensure equal, content-fitting heights across all cards
112+
"h-full",
113113
"md:backdrop-blur-lg",
114114
// All core team members get the pretty pink border
115115
"border-2 border-[var(--brand-pink)]/40 shadow-[0_0_20px_rgba(228,90,146,0.2)]",
@@ -159,7 +159,7 @@ function TeamCard({
159159
{/* Header with role, name, and LinkedIn */}
160160
<div className="flex items-start justify-between gap-2 mb-2">
161161
<div className="flex-1 min-w-0">
162-
<span className="text-[0.6rem] sm:text-[0.7rem] font-black uppercase tracking-[0.1em] text-[var(--brand-pink)] mb-0.5 block">
162+
<span className="text-[0.6rem] sm:text-[0.7rem] font-black uppercase tracking-[0.1em] text-[var(--brand-pink)] mb-1 block leading-normal">
163163
{member.role}
164164
</span>
165165
<h3 className="font-display text-lg sm:text-xl md:text-2xl font-bold tracking-tight leading-tight">

components/ui/glowing-ai-chat-assistant.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation"
66
import ReactMarkdown from "react-markdown"
77
import remarkGfm from "remark-gfm"
88

9-
import { Mic, Send, Info, Bot, X } from "lucide-react"
9+
import { Mic, Send, Info, Bot, X, Trash } from "lucide-react"
1010

1111
interface ChatMessage {
1212
id: number
@@ -15,7 +15,7 @@ interface ChatMessage {
1515
}
1616

1717
const MAX_CHARS = 2000
18-
const MAX_HISTORY = 40
18+
const MAX_HISTORY = 8
1919
const STORAGE_KEY = "bb-floating-assistant-state-v1"
2020
const QUICK_PROMPTS = [
2121
"What is Bits&Bytes?",
@@ -229,6 +229,7 @@ const FloatingAiAssistant: React.FC = () => {
229229
const decoder = new TextDecoder()
230230
let buffer = ""
231231
let navigatePath: string | null = null
232+
let highlightSnippet: string | null = null
232233

233234
while (true) {
234235
const { value, done } = await reader.read()
@@ -264,6 +265,7 @@ const FloatingAiAssistant: React.FC = () => {
264265
if (actionData?.type === "navigate" && typeof actionData.path === "string") {
265266
navigatePath = actionData.path
266267
} else if (actionData?.type === "highlight" && typeof actionData.textSnippet === "string") {
268+
highlightSnippet = actionData.textSnippet
267269
setTimeout(() => {
268270
const text = actionData.textSnippet as string
269271
if ((window as any).find && (window as any).find(text)) {
@@ -315,11 +317,12 @@ const FloatingAiAssistant: React.FC = () => {
315317
}
316318
}
317319

318-
updateMessageContent(assistantMessageId, (prev) =>
319-
prev && prev.trim().length > 0
320-
? prev
321-
: "I'm not sure about that based on the information publicly available on this site."
322-
)
320+
updateMessageContent(assistantMessageId, (prev) => {
321+
if (prev && prev.trim().length > 0) return prev
322+
if (navigatePath) return "Taking you there! 🚀"
323+
if (highlightSnippet) return "Here's what I found for you! ✨"
324+
return "I'm not sure about that based on the information publicly available on this site."
325+
})
323326

324327
if (navigatePath) {
325328
router.push(navigatePath)
@@ -479,6 +482,19 @@ const FloatingAiAssistant: React.FC = () => {
479482
<span className="rounded-2xl bg-zinc-800/70 px-2 py-1 text-[0.65rem] font-medium text-zinc-200">
480483
{modelName}
481484
</span>
485+
<button
486+
onClick={() => {
487+
setMessages([])
488+
setCharCount(0)
489+
setMessage("")
490+
window.localStorage.removeItem(STORAGE_KEY)
491+
}}
492+
className="inline-flex h-7 w-7 items-center justify-center rounded-full text-zinc-400 hover:bg-zinc-800/80 hover:text-red-400 transition-colors"
493+
aria-label="Clear chat"
494+
title="Clear chat"
495+
>
496+
<Trash className="h-3.5 w-3.5" />
497+
</button>
482498
<button
483499
onClick={() => {
484500
streamControllerRef.current?.abort()

components/ui/hero-futuristic.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,19 @@ export const HeroFuturistic = () => {
6060
<div className="flex flex-col gap-4 sm:flex-row w-full mt-2">
6161
<Button
6262
asChild
63-
className="h-14 px-8 rounded-full bg-(--brand-pink) text-base font-bold text-white shadow-[0_0_30px_rgba(228,90,146,0.5)] hover:shadow-[0_0_50px_rgba(228,90,146,0.7)] transition-all hover:scale-[1.02] active:scale-[0.98]"
63+
className="w-full sm:flex-1 h-14 px-8 rounded-full bg-(--brand-pink) text-base font-bold text-white shadow-[0_0_30px_rgba(228,90,146,0.5)] hover:shadow-[0_0_50px_rgba(228,90,146,0.7)] transition-all hover:scale-[1.02] active:scale-[0.98]"
6464
>
65-
<Link href="/join">
65+
<Link href="/join" className="flex items-center justify-center gap-2">
6666
Join the crew
67-
<ArrowRight className="ml-2 h-5 w-5" />
67+
<ArrowRight className="h-5 w-5 shrink-0" />
6868
</Link>
6969
</Button>
7070
<Button
7171
asChild
7272
variant="outline"
73-
className="h-14 px-8 rounded-full border-white/20 bg-white/5 text-base font-semibold text-white backdrop-blur-md hover:bg-white/10 transition-all hover:scale-[1.02]"
73+
className="w-full sm:flex-1 h-14 px-8 rounded-full border-white/20 bg-white/5 text-base font-semibold text-white backdrop-blur-md hover:bg-white/10 transition-all hover:scale-[1.02]"
7474
>
75-
<Link href="/impact">See what we&apos;ve built</Link>
75+
<Link href="/impact" className="flex items-center justify-center">See what we&apos;ve built</Link>
7676
</Button>
7777
</div>
7878

0 commit comments

Comments
 (0)