Skip to content

Commit 87683b1

Browse files
committed
feat(add): updated docs and stuff
1 parent 5717b0d commit 87683b1

42 files changed

Lines changed: 2608 additions & 1649 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,49 @@ All notable changes to FixFX will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.4.0] - 2026-03-10
9+
10+
### Added
11+
12+
#### Fixie AI Chat
13+
14+
- **Chat Page** (`/chat`) — New full-screen AI assistant "Fixie" for the CitizenFX ecosystem with real-time streaming responses, message history, and persistent saved chats (localStorage)
15+
- **Chat API Route** (`/api/chat`) — Streaming AI endpoint supporting 8 models across three providers (OpenAI, Anthropic, Google) with model allowlisting and max 4096 token responses
16+
- **BYOK (Bring Your Own Key)** — Users can supply their own API keys for Anthropic and Google to unlock Claude and Gemini models; keys stored in localStorage and never sent to the server unless used for that provider's request
17+
- **8 AI Models** — GPT-4o, GPT-4o Mini, GPT-4 Turbo, GPT-3.5 Turbo (OpenAI); Claude 3.5 Sonnet, Claude 3 Haiku (Anthropic, BYOK); Gemini 2.0 Flash, Gemini 1.5 Flash (Google, BYOK)
18+
- **ChatInterface Component** — Auto-growing textarea, Enter-to-send / Shift+Enter for newline, markdown rendering with syntax-highlighted code blocks, copy-to-clipboard, typing indicator, and message timestamps
19+
- **Chat Sidebar** — Collapsible desktop sidebar (`w-72` / `w-14`) with two-tab layout (Chats / Settings), navigation links, recent chat history (up to 12), model selector, temperature slider, and BYOK key management
20+
- **Mobile Chat Header** — Fixed top bar with menu button, Fixie branding, and active model badge
21+
- **Mobile Chat Drawer** — Sheet-based mobile drawer mirroring the desktop sidebar's two-tab structure
22+
- **Chat Layout & SEO** — Dedicated layout with metadata, Open Graph tags, Twitter image, and canonical URL for `/chat`
23+
- **Chat Persistence** — Chats auto-save to localStorage with deduplication, active chat restoration on page load, and custom events (`chatsUpdated`, `activeChatChanged`, `byokChanged`) for cross-component sync
24+
25+
#### Game References
26+
27+
- **Vehicle Models** (`/game-references/vehicle-models`) — Searchable reference page for all GTA V vehicle models with category filtering and pagination
28+
- **Vehicle Colours** (`/game-references/vehicle-colours`) — Searchable reference page for vehicle colours with type filtering and pagination
29+
- **Vehicle Flags** (`/game-references/vehicle-flags`) — Searchable reference page for vehicle flags with pagination
30+
31+
#### Navigation
32+
33+
- **NAV_LINKS Consistency** — Added Game References, Validator, Hosting, and Chat to the global `NAV_LINKS` constant so all sidebars and navigation menus share the same link set
34+
35+
### Changed
36+
37+
#### Fixie AI Chat
38+
39+
- **Tabbed Sidebar Layout** — Desktop sidebar uses a two-tab design (Chats / Settings) to separate navigation and chat history from model configuration and API keys, preventing clutter as chat history grows
40+
- **Wider Chat Area** — Message area and input widened from `max-w-5xl` to `max-w-7xl` so conversations use more screen real estate on large displays
41+
- **Collapsed Sidebar** — Collapsed state shows icon-only navigation links with tooltips, plus History and Settings expand buttons separated by a divider
42+
43+
### Fixed
44+
45+
#### Fixie AI Chat
46+
47+
- **Encoding Artifacts** — Fixed garbled text in sidebar (`·``·`, `â€"```) caused by previous encoding issue
48+
49+
---
50+
851
## [1.3.0] - 2026-03-04
952

1053
### Added

app/api/chat/route.ts

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,59 @@
1-
import { anthropic } from "@ai-sdk/anthropic";
2-
import { google } from "@ai-sdk/google";
3-
import { openai } from "@ai-sdk/openai";
1+
import { createAnthropic } from "@ai-sdk/anthropic";
2+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
3+
import { createOpenAI, openai } from "@ai-sdk/openai";
44
import { geolocation, ipAddress } from "@vercel/functions";
55
import { convertToCoreMessages, streamText } from "ai";
66
import { z } from "zod";
77

88
export const maxDuration = 30;
99

10+
const ALLOWED_MODELS = new Set([
11+
"gpt-4o",
12+
"gpt-4o-mini",
13+
"gpt-4-turbo",
14+
"gpt-3.5-turbo",
15+
"gemini-2.0-flash",
16+
"gemini-1.5-flash",
17+
"claude-3-5-sonnet",
18+
"claude-3-haiku",
19+
]);
20+
21+
interface ByokKeys {
22+
openai?: string;
23+
anthropic?: string;
24+
google?: string;
25+
}
26+
1027
export async function POST(req: Request) {
1128
const {
1229
messages,
1330
model = "gpt-4o-mini",
1431
temperature = 0.7,
15-
} = await req.json();
32+
byok,
33+
}: { messages: any[]; model: string; temperature: number; byok?: ByokKeys } =
34+
await req.json();
35+
36+
// Validate model to prevent arbitrary model injection
37+
if (!ALLOWED_MODELS.has(model)) {
38+
return new Response(JSON.stringify({ error: "Invalid model" }), {
39+
status: 400,
40+
});
41+
}
42+
1643
const { city, latitude, longitude } = geolocation(req);
17-
const ip = ipAddress(req);
44+
// NOTE: byok keys are intentionally never logged
45+
void ipAddress(req);
46+
47+
// Build provider instances — use BYOK key if provided, fall back to server env key
48+
const openaiProvider = (byok?.openai?.trim())
49+
? createOpenAI({ apiKey: byok.openai.trim() })
50+
: openai;
51+
const anthropicProvider = createAnthropic({
52+
apiKey: byok?.anthropic?.trim() || process.env.ANTHROPIC_API_KEY || "",
53+
});
54+
const googleProvider = createGoogleGenerativeAI({
55+
apiKey: byok?.google?.trim() || process.env.GOOGLE_GENERATIVE_AI_API_KEY || "",
56+
});
1857

1958
const system = `You are Fixie, a specialized assistant for everything CitizenFX.
2059
@@ -87,51 +126,46 @@ You should NOT:
87126
The user's current location is ${city} at latitude ${latitude} and longitude ${longitude}.
88127
Today's date and day is ${new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}.`;
89128

90-
console.log({ messages, model, temperature });
129+
console.log({ model, temperature });
130+
131+
const geminiSafetySettings = [
132+
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
133+
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
134+
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
135+
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" },
136+
] as const;
91137

92138
let selectedModel;
93139
if (model === "gpt-4o") {
94-
selectedModel = openai("gpt-4o");
140+
selectedModel = openaiProvider("gpt-4o");
95141
} else if (model === "gpt-4o-mini") {
96-
selectedModel = openai("gpt-4o-mini");
142+
selectedModel = openaiProvider("gpt-4o-mini");
97143
} else if (model === "gpt-4-turbo") {
98-
selectedModel = openai("gpt-4-turbo");
144+
selectedModel = openaiProvider("gpt-4-turbo");
99145
} else if (model === "gpt-3.5-turbo") {
100-
selectedModel = openai("gpt-3.5-turbo");
146+
selectedModel = openaiProvider("gpt-3.5-turbo");
147+
} else if (model === "gemini-2.0-flash") {
148+
selectedModel = googleProvider("gemini-2.0-flash", {
149+
safetySettings: geminiSafetySettings,
150+
});
101151
} else if (model === "gemini-1.5-flash") {
102-
selectedModel = google("models/gemini-1.5-flash-latest", {
103-
safetySettings: [
104-
{
105-
category: "HARM_CATEGORY_HARASSMENT",
106-
threshold: "BLOCK_NONE",
107-
},
108-
{
109-
category: "HARM_CATEGORY_HATE_SPEECH",
110-
threshold: "BLOCK_NONE",
111-
},
112-
{
113-
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
114-
threshold: "BLOCK_NONE",
115-
},
116-
{
117-
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
118-
threshold: "BLOCK_NONE",
119-
},
120-
],
152+
selectedModel = googleProvider("gemini-1.5-flash-latest", {
153+
safetySettings: geminiSafetySettings,
121154
});
155+
} else if (model === "claude-3-5-sonnet") {
156+
selectedModel = anthropicProvider("claude-3-5-sonnet-20241022");
122157
} else if (model === "claude-3-haiku") {
123-
selectedModel = anthropic("claude-3-haiku-20240307");
158+
selectedModel = anthropicProvider("claude-3-haiku-20240307");
124159
} else {
125-
// Default to GPT-4o mini as fallback
126-
selectedModel = openai("gpt-4o-mini");
160+
selectedModel = openaiProvider("gpt-4o-mini");
127161
}
128162

129163
const result = await streamText({
130164
model: selectedModel,
131165
messages: convertToCoreMessages(messages),
132166
temperature,
133167
system,
134-
maxTokens: 1000,
168+
maxTokens: 4096,
135169
experimental_toolCallStreaming: true,
136170
tools: {
137171
weatherTool: {
@@ -155,12 +189,10 @@ Today's date and day is ${new Date().toLocaleDateString("en-US", { weekday: "lon
155189
latitude: number;
156190
longitude: number;
157191
}) => {
158-
console.log(latitude, longitude);
159192
const response = await fetch(
160193
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,apparent_temperature,rain`,
161194
);
162195
const data = await response.json();
163-
console.log(data);
164196
return {
165197
temperature: data.current.temperature_2m,
166198
apparentTemperature: data.current.apparent_temperature,
@@ -197,9 +229,7 @@ Today's date and day is ${new Date().toLocaleDateString("en-US", { weekday: "lon
197229
const apiKey = process.env.TAVILY_API_KEY;
198230
const response = await fetch("https://api.tavily.com/search", {
199231
method: "POST",
200-
headers: {
201-
"Content-Type": "application/json",
202-
},
232+
headers: { "Content-Type": "application/json" },
203233
body: JSON.stringify({
204234
api_key: apiKey,
205235
query,
@@ -210,12 +240,12 @@ Today's date and day is ${new Date().toLocaleDateString("en-US", { weekday: "lon
210240
}),
211241
});
212242
const data = await response.json();
213-
let context = data.results.map(
243+
const context = data.results.map(
214244
(obj: {
215-
url: any;
216-
content: any;
217-
title: any;
218-
raw_content: any;
245+
url: string;
246+
content: string;
247+
title: string;
248+
raw_content: string;
219249
}) => ({
220250
url: obj.url,
221251
title: obj.title,
@@ -238,16 +268,12 @@ Today's date and day is ${new Date().toLocaleDateString("en-US", { weekday: "lon
238268
}),
239269
execute: async ({ code }) => {
240270
code = code.replace(/\\n/g, "\n").replace(/\\/g, "");
241-
console.log(code);
242271
const response = await fetch("https://interpreter.za16.co", {
243272
method: "POST",
244-
headers: {
245-
"Content-Type": "application/json",
246-
},
273+
headers: { "Content-Type": "application/json" },
247274
body: JSON.stringify({ code }),
248275
});
249276
const data = await response.json();
250-
console.log(data.std_out);
251277
return {
252278
output: data.std_out,
253279
error: data.error,
@@ -263,3 +289,4 @@ Today's date and day is ${new Date().toLocaleDateString("en-US", { weekday: "lon
263289

264290
return result.toAIStreamResponse();
265291
}
292+

app/chat/page.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,6 @@ export default function AskPage() {
125125

126126
return (
127127
<div className="relative flex min-h-screen h-screen bg-fd-background overflow-hidden">
128-
{/* Ambient background */}
129-
<div className="absolute inset-0 -z-10 pointer-events-none overflow-hidden">
130-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(88,101,242,0.08),rgba(255,255,255,0))]" />
131-
<div className="absolute -left-20 -top-20 h-64 w-64 animate-pulse rounded-full bg-[#5865F2]/10 blur-[100px]" />
132-
<div className="absolute -bottom-20 -right-20 h-64 w-64 animate-pulse rounded-full bg-[#10B981]/10 blur-[100px]" />
133-
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-96 w-96 rounded-full bg-[#5865F2]/5 blur-[150px]" />
134-
</div>
135-
136128
{/* Desktop layout */}
137129
<div className="hidden md:flex w-full h-full">
138130
<ChatSidebar

app/game-references/_components/page-shell.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,15 @@ interface ReferenceFilterChipsProps {
182182
options: string[];
183183
value: string;
184184
onChange: (v: string) => void;
185+
formatLabel?: (opt: string) => string;
185186
}
186187

187188
export function ReferenceFilterChips({
188189
allLabel = "All",
189190
options,
190191
value,
191192
onChange,
193+
formatLabel,
192194
}: ReferenceFilterChipsProps) {
193195
if (options.length === 0) return null;
194196
return (
@@ -207,7 +209,7 @@ export function ReferenceFilterChips({
207209
size="sm"
208210
onClick={() => onChange(opt)}
209211
>
210-
{opt}
212+
{formatLabel ? formatLabel(opt) : opt}
211213
</Button>
212214
))}
213215
</div>

app/game-references/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
ArrowRight,
1818
BookOpen,
1919
Database,
20+
Car,
21+
PaintBucket,
22+
Flag,
2023
} from "lucide-react";
2124
import { useFetch } from "@core/useFetch";
2225
import { API_URL } from "@/packages/utils/src/constants/link";
@@ -130,6 +133,33 @@ const REFERENCES = [
130133
iconColor: "text-teal-500",
131134
tag: "zones",
132135
},
136+
{
137+
slug: "vehicle-models",
138+
label: "Vehicle Models",
139+
description: "All GTA V / FiveM vehicle model names with hashes, organised by category.",
140+
icon: Car,
141+
color: "from-blue-500/20 to-blue-600/5 border-blue-500/20",
142+
iconColor: "text-blue-500",
143+
tag: "vehicle-models",
144+
},
145+
{
146+
slug: "vehicle-colours",
147+
label: "Vehicle Colours",
148+
description: "All vehicle paint colour indices grouped by type — metallic, matte, metals, and unnamed.",
149+
icon: PaintBucket,
150+
color: "from-orange-500/20 to-orange-600/5 border-orange-500/20",
151+
iconColor: "text-orange-500",
152+
tag: "vehicle-colours",
153+
},
154+
{
155+
slug: "vehicle-flags",
156+
label: "Vehicle Flags",
157+
description: "All vehicle flag definitions with descriptions and the build version they were introduced in.",
158+
icon: Flag,
159+
color: "from-red-500/20 to-red-600/5 border-red-500/20",
160+
iconColor: "text-red-500",
161+
tag: "vehicle-flags",
162+
},
133163
];
134164

135165
interface SummaryData {

0 commit comments

Comments
 (0)