-
{selectedJob.job_title}
-
- {selectedJob.industry}
-
-
{selectedJob.description}
-
- {selectedJob.keywords.map((kw, i) => (
-
- {kw}
-
- ))}
-
+
+ {selectedJob && (
+
+
{selectedJob.job_title}
+
{selectedJob.industry}
+
{selectedJob.description}
+
+ {selectedJob.keywords.map((kw, i) => (
+ {kw}
+ ))}
- )}
-
-
-
-
- Tips
-
-
- -
-
- Ask for cold email templates for a specific job
-
- -
-
- Request objection handling tips
-
- -
-
- Ask for outreach strategies per industry
-
-
+
+ )}
+
+
+
+
+ Quick Tips
+
+ {tips.map((tip, i) => (
+
handleTipClick(tip)}
+ className="flex items-start gap-2.5 bg-card/50 hover:bg-card border border-border hover:border-primary/20 rounded-xl p-3.5 mb-2 cursor-pointer transition-all duration-200 group"
+ >
+
+ {tip}
+
+ ))}
+
+
+
+
+ Recent Prompts
+
+ {recentPrompts.length > 0 ? (
+ recentPrompts.map((prompt, i) => (
+
handleRecentPromptClick(prompt)}
+ className="bg-card/50 rounded-xl p-3 mb-2 border border-border text-muted-foreground text-xs truncate hover:text-foreground cursor-pointer transition-colors duration-200"
+ >
+ {prompt}
+
+ ))
+ ) : (
+
Your recent prompts will appear here
+ )}
+
+
diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx
index 52cafb4..cb288a3 100644
--- a/src/app/(dashboard)/dashboard/page.tsx
+++ b/src/app/(dashboard)/dashboard/page.tsx
@@ -86,7 +86,7 @@ export default function DashboardPage() {
{stats
@@ -97,7 +97,7 @@ export default function DashboardPage() {
}
diff --git a/src/app/api/ai/giphy/route.ts b/src/app/api/ai/giphy/route.ts
new file mode 100644
index 0000000..be69b06
--- /dev/null
+++ b/src/app/api/ai/giphy/route.ts
@@ -0,0 +1,57 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getSessionUser } from "@/lib/auth"
+
+const GIPHY_API_KEY = process.env.GIPHY_API_KEY
+const GIPHY_BASE = "https://api.giphy.com/v1/gifs"
+
+export async function GET(request: NextRequest) {
+ try {
+ const user = await getSessionUser()
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+ }
+
+ if (!GIPHY_API_KEY) {
+ return NextResponse.json({ error: "GIPHY API key not configured" }, { status: 500 })
+ }
+
+ const { searchParams } = new URL(request.url)
+ const type = searchParams.get("type") || "trending"
+ const query = searchParams.get("q") || ""
+ const offset = parseInt(searchParams.get("offset") || "0", 10)
+ const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 50)
+
+ let url: string
+ if (type === "search" && query) {
+ url = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&rating=g`
+ } else {
+ url = `${GIPHY_BASE}/trending?api_key=${GIPHY_API_KEY}&limit=${limit}&offset=${offset}&rating=g`
+ }
+
+ const res = await fetch(url)
+ if (!res.ok) {
+ const errBody = await res.text()
+ console.error("GIPHY API error:", res.status, errBody)
+ return NextResponse.json({ error: `GIPHY API error (${res.status}): ${errBody}` }, { status: res.status })
+ }
+
+ const data = await res.json()
+ const gifs = (data.data || []).map((gif: any) => ({
+ id: gif.id,
+ title: gif.title,
+ url: gif.images?.original?.url || "",
+ previewUrl: gif.images?.fixed_width?.url || "",
+ previewHeight: gif.images?.fixed_width?.height || 150,
+ width: gif.images?.original?.width || 0,
+ height: gif.images?.original?.height || 0,
+ }))
+
+ return NextResponse.json({
+ gifs,
+ pagination: data.pagination || { total_count: 0, count: gifs.length, offset },
+ })
+ } catch (error: any) {
+ console.error("GIPHY proxy error:", error)
+ return NextResponse.json({ error: error.message || "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/src/app/api/ai/health/route.ts b/src/app/api/ai/health/route.ts
new file mode 100644
index 0000000..031c367
--- /dev/null
+++ b/src/app/api/ai/health/route.ts
@@ -0,0 +1,7 @@
+import { NextResponse } from "next/server"
+import { checkAiServiceStatus } from "@/lib/ai"
+
+export async function GET() {
+ const ok = await checkAiServiceStatus()
+ return NextResponse.json({ status: ok ? "ok" : "unavailable" }, { status: ok ? 200 : 503 })
+}
diff --git a/src/app/api/gifs/route.ts b/src/app/api/gifs/route.ts
index 8ecf18e..1b7c5b6 100644
--- a/src/app/api/gifs/route.ts
+++ b/src/app/api/gifs/route.ts
@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server"
import { getSessionUser } from "@/lib/auth"
-const TENOR_API_KEY = process.env.TENOR_API_KEY || ""
-const TENOR_BASE = "https://tenor.googleapis.com/v2"
+const GIPHY_API_KEY = process.env.GIPHY_API_KEY
+const GIPHY_BASE = "https://api.giphy.com/v1/gifs"
export async function GET(request: NextRequest) {
try {
@@ -12,41 +12,78 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const q = searchParams.get("q") || ""
const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 50)
- const pos = searchParams.get("pos") || ""
+ const pos = parseInt(searchParams.get("pos") || "0", 10) || 0
- if (!TENOR_API_KEY) {
+ if (!GIPHY_API_KEY) {
+ console.error("Missing GIPHY_API_KEY environment variable")
return NextResponse.json({ results: [], noKey: true })
}
- const endpoint = q
- ? `${TENOR_BASE}/search?q=${encodeURIComponent(q)}&key=${TENOR_API_KEY}&limit=${limit}&media_filter=minimal`
- : `${TENOR_BASE}/featured?key=${TENOR_API_KEY}&limit=${limit}&media_filter=minimal`
+ let data: any
- const url = pos ? `${endpoint}&pos=${pos}` : endpoint
+ if (q) {
+ const url = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=${encodeURIComponent(q)}&limit=${limit}&offset=${pos}&rating=g`
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
+ if (!res.ok) {
+ const errBody = await res.text()
+ console.error("GIPHY API search error:", res.status, errBody)
+ return NextResponse.json({ results: [], error: `GIPHY error: ${errBody}` }, { status: 502 })
+ }
+ data = await res.json()
+ } else {
+ const trendingUrl = `${GIPHY_BASE}/trending?api_key=${GIPHY_API_KEY}&limit=${limit}&offset=${pos}&rating=g`
+ const trendingRes = await fetch(trendingUrl, { signal: AbortSignal.timeout(8000) })
- const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
- if (!res.ok) {
- return NextResponse.json({ results: [], error: "Tenor API error" }, { status: 502 })
+ if (trendingRes.ok) {
+ data = await trendingRes.json()
+ if (!data.data?.length && pos === 0) {
+ console.log("Trending returned no results, falling back to default search")
+ const fallbackUrl = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=funny&limit=${limit}&offset=${pos}&rating=g`
+ const fallbackRes = await fetch(fallbackUrl, { signal: AbortSignal.timeout(8000) })
+ if (!fallbackRes.ok) {
+ const errBody = await fallbackRes.text()
+ console.error("GIPHY fallback search error:", fallbackRes.status, errBody)
+ return NextResponse.json({ results: [], error: `GIPHY error: ${errBody}` }, { status: 502 })
+ }
+ data = await fallbackRes.json()
+ }
+ } else {
+ const errBody = await trendingRes.text()
+ console.error("GIPHY trending error:", trendingRes.status, errBody)
+ console.log("Trending failed, falling back to default search")
+ const fallbackUrl = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=funny&limit=${limit}&offset=${pos}&rating=g`
+ const fallbackRes = await fetch(fallbackUrl, { signal: AbortSignal.timeout(8000) })
+ if (!fallbackRes.ok) {
+ const fallbackErr = await fallbackRes.text()
+ console.error("GIPHY fallback search error:", fallbackRes.status, fallbackErr)
+ return NextResponse.json({ results: [], error: `GIPHY error: ${fallbackErr}` }, { status: 502 })
+ }
+ data = await fallbackRes.json()
+ }
}
-
- const data = await res.json()
-
- const results = (data.results || []).map((item: any) => {
- const media = item.media_formats?.gif || item.media_formats?.tinygif || {}
- const preview = item.media_formats?.tinygif || item.media_formats?.gif || {}
+ const results = (data.data || []).map((item: any) => {
+ const dims = item.images?.original
return {
id: item.id,
title: item.title || "",
- url: media.url || "",
- previewUrl: preview.url || "",
- width: media.dims?.[0] || 200,
- height: media.dims?.[1] || 200,
+ url: dims?.url || "",
+ previewUrl: item.images?.fixed_width?.url || "",
+ width: dims?.width ? parseInt(dims.width, 10) : 200,
+ height: dims?.height ? parseInt(dims.height, 10) : 200,
}
})
- return NextResponse.json({ results, next: data.next || "" })
- } catch (error) {
- console.error("GIF API error:", error)
- return NextResponse.json({ results: [], error: "Failed to fetch GIFs" }, { status: 500 })
+ const nextOffset = pos + results.length
+ const total = data.pagination?.total_count || 0
+ const hasMore = total ? nextOffset < total : results.length === limit
+ const nextPos = hasMore ? String(nextOffset) : ""
+
+ return NextResponse.json({ results, next: nextPos })
+ } catch (error: any) {
+ console.error("GIF proxy error:", error)
+ const message = error?.message === "Failed to fetch"
+ ? "Could not reach the GIF service."
+ : "Failed to fetch GIFs."
+ return NextResponse.json({ results: [], error: message }, { status: 500 })
}
}
diff --git a/src/app/call/[roomId]/page.tsx b/src/app/call/[roomId]/page.tsx
index 780d963..f438469 100644
--- a/src/app/call/[roomId]/page.tsx
+++ b/src/app/call/[roomId]/page.tsx
@@ -53,24 +53,24 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
if (!joined) {
return (
-
-
+
+
{connectionTimeout ? (
<>
-
This call is no longer available.
+
This call is no longer available.
>
) : !isReady ? (
<>
-
-
Connecting to call...
+
+
Connecting to call...
>
) : (
<>
-
Join Call
+
Join Call
{micError && (
-
{micError}
+
{micError}
)}
setDisplayName(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") handleJoin() }}
placeholder="Enter your name"
- className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-4 outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]"
+ className="bg-[#FAFAF6] dark:bg-[#1A1A1A] border border-[#D4D8CC] dark:border-[#333333] text-[#2D3020] dark:text-white placeholder-[#8A9078] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-4 outline-none transition-colors focus:border-[#C84B4B] dark:focus:border-[#FF4444]"
/>