Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/server/convex/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import { createLogger } from "./lib/logger";
import { rateLimiter } from "./lib/rateLimiter";
Expand Down
30 changes: 0 additions & 30 deletions apps/server/convex/lib/redisRest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,36 +63,6 @@ function getRedisCredentials(): { url: string; token: string } | null {
return { url, token };
}

/**
* Execute a Redis command via REST API
* Note: Currently unused but kept for future single-command operations
*/
async function _executeCommand<T = unknown>(
command: (string | number)[]
): Promise<T> {
const creds = getRedisCredentials();
if (!creds) {
throw new Error("Redis credentials not configured");
}

const response = await fetch(creds.url, {
method: "POST",
headers: {
Authorization: `Bearer ${creds.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(command),
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Redis command failed: ${response.status} - ${errorText}`);
}

const data = await response.json();
return data.result as T;
}

/**
* Execute multiple Redis commands in a pipeline
*/
Expand Down
20 changes: 0 additions & 20 deletions apps/server/convex/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,6 @@ import { getProfileByUserId, getOrCreateProfile } from "./lib/profiles";
import { authComponent } from "./auth";
import { components } from "./_generated/api";

// User document validator with all fields including fileUploadCount
// Note: Kept for potential future use (e.g., admin queries that need raw user data)
const _userDoc = v.object({
_id: v.id("users"),
_creationTime: v.number(),
externalId: v.string(),
email: v.optional(v.string()),
name: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
encryptedOpenRouterKey: v.optional(v.string()),
fileUploadCount: v.optional(v.number()),
// Ban fields
banned: v.optional(v.boolean()),
bannedAt: v.optional(v.number()),
banReason: v.optional(v.string()),
banExpiresAt: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
});

// User with profile data (for backwards-compatible responses)
// Includes merged profile data that prefers profile over user during migration
const userWithProfileDoc = v.object({
Expand Down
5 changes: 4 additions & 1 deletion apps/web/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@

import { tanstackConfig } from '@tanstack/eslint-config'

export default [...tanstackConfig]
export default [
{ ignores: ['.output/**', 'dist/**', '.vinxi/**', '*.config.js'] },
...tanstackConfig,
]
4 changes: 2 additions & 2 deletions apps/web/src/components/ai-elements/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

"use client";

import { cn } from "@/lib/utils";
import { ArrowDownIcon } from "lucide-react";
import type { ComponentProps, RefObject } from "react";
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
import type { ComponentProps, RefObject } from "react";
import { cn } from "@/lib/utils";

// ============================================================================
// Context for scroll state
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/ai-elements/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

"use client";

import { cn } from "@/lib/utils";
import type { ComponentProps, ReactNode } from "react";
import { createContext, useContext } from "react";
import { Streamdown } from "streamdown";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";

// ============================================================================
// Context
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ai-elements/model-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ComponentProps, ReactNode } from "react";
import {
Command,
CommandDialog,
Expand All @@ -11,7 +12,6 @@ import {
} from "@/components/ui/command";
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import type { ComponentProps, ReactNode } from "react";

export type ModelSelectorProps = ComponentProps<typeof Dialog>;

Expand Down
121 changes: 59 additions & 62 deletions apps/web/src/components/ai-elements/prompt-input.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
"use client";

import {
CornerDownLeftIcon,
ImageIcon,
Loader2Icon,
MicIcon,
PaperclipIcon,
PlusIcon,
SquareIcon,
XIcon,
} from "lucide-react";
import { motion } from "motion/react";
import { nanoid } from "nanoid";
import {


Children,




Fragment,





createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from "react";
import type { ChatStatus, FileUIPart } from "ai";
import type {ChangeEvent, ChangeEventHandler, ClipboardEventHandler, ComponentProps, FormEvent, FormEventHandler, HTMLAttributes, KeyboardEventHandler, PropsWithChildren, ReactNode, RefObject} from "react";
import { Button } from "@/components/ui/button";
import {
Command,
Expand Down Expand Up @@ -31,49 +67,14 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import type { ChatStatus, FileUIPart } from "ai";
import {
CornerDownLeftIcon,
ImageIcon,
Loader2Icon,
MicIcon,
PaperclipIcon,
PlusIcon,
SquareIcon,
XIcon,
} from "lucide-react";
import { motion } from "motion/react";
import { nanoid } from "nanoid";
import {
type ChangeEvent,
type ChangeEventHandler,
Children,
type ClipboardEventHandler,
type ComponentProps,
createContext,
type FormEvent,
type FormEventHandler,
Fragment,
type HTMLAttributes,
type KeyboardEventHandler,
type PropsWithChildren,
type ReactNode,
type RefObject,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";

// ============================================================================
// Provider Context & Types
// ============================================================================

export type AttachmentsContext = {
files: (FileUIPart & { id: string })[];
add: (files: File[] | FileList) => void;
files: Array<FileUIPart & { id: string }>;
add: (files: Array<File> | FileList) => void;
remove: (id: string) => void;
clear: () => void;
openFileDialog: () => void;
Expand Down Expand Up @@ -138,11 +139,11 @@ export function PromptInputProvider({
const clearInput = useCallback(() => setTextInput(""), []);

// ----- attachments state (global when wrapped)
const [attachmentFiles, setAttachmentFiles] = useState<(FileUIPart & { id: string })[]>([]);
const [attachmentFiles, setAttachmentFiles] = useState<Array<FileUIPart & { id: string }>>([]);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const openRef = useRef<() => void>(() => {});

const add = useCallback((files: File[] | FileList) => {
const add = useCallback((files: Array<File> | FileList) => {
const incoming = Array.from(files);
if (incoming.length === 0) {
return;
Expand Down Expand Up @@ -198,7 +199,7 @@ export function PromptInputProvider({
}, []);

const openFileDialog = useCallback(() => {
openRef.current?.();
openRef.current();
}, []);

const attachments = useMemo<AttachmentsContext>(
Expand Down Expand Up @@ -272,7 +273,7 @@ export function PromptInputAttachment({ data, className, ...props }: PromptInput

const filename = data.filename || "";

const mediaType = data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
const mediaType = data.mediaType.startsWith("image/") && data.url ? "image" : "file";
const isImage = mediaType === "image";

const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
Expand Down Expand Up @@ -475,7 +476,7 @@ export const PromptInputAttachmentButton = ({

export type PromptInputMessage = {
text: string;
files: FileUIPart[];
files: Array<FileUIPart>;
};

export type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit" | "onError"> & {
Expand Down Expand Up @@ -517,7 +518,7 @@ export const PromptInput = ({
const formRef = useRef<HTMLFormElement | null>(null);

// ----- Local attachments (only used when no provider)
const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]);
const [items, setItems] = useState<Array<FileUIPart & { id: string }>>([]);
const files = usingProvider ? controller.attachments.files : items;

// Keep a ref to files for cleanup on unmount (avoids stale closure)
Expand Down Expand Up @@ -551,7 +552,7 @@ export const PromptInput = ({
);

const addLocal = useCallback(
(fileList: File[] | FileList) => {
(fileList: Array<File> | FileList) => {
const incoming = Array.from(fileList);
const accepted = incoming.filter((f) => matchesAccept(f));
if (incoming.length && accepted.length === 0) {
Expand Down Expand Up @@ -581,7 +582,7 @@ export const PromptInput = ({
message: "Too many files. Some were not added.",
});
}
const next: (FileUIPart & { id: string })[] = [];
const next: Array<FileUIPart & { id: string }> = [];
for (const file of capped) {
next.push({
id: nanoid(),
Expand Down Expand Up @@ -650,12 +651,12 @@ export const PromptInput = ({
if (globalDrop) return; // when global drop is on, let the document-level handler own drops

const onDragOver = (e: DragEvent) => {
if (e.dataTransfer?.types?.includes("Files")) {
if (e.dataTransfer?.types.includes("Files")) {
e.preventDefault();
}
};
const onDrop = (e: DragEvent) => {
if (e.dataTransfer?.types?.includes("Files")) {
if (e.dataTransfer?.types.includes("Files")) {
e.preventDefault();
}
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
Expand All @@ -674,12 +675,12 @@ export const PromptInput = ({
if (!globalDrop) return;

const onDragOver = (e: DragEvent) => {
if (e.dataTransfer?.types?.includes("Files")) {
if (e.dataTransfer?.types.includes("Files")) {
e.preventDefault();
}
};
const onDrop = (e: DragEvent) => {
if (e.dataTransfer?.types?.includes("Files")) {
if (e.dataTransfer?.types.includes("Files")) {
e.preventDefault();
}
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
Expand All @@ -702,7 +703,7 @@ export const PromptInput = ({
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- cleanup only on unmount; filesRef always current
// cleanup only on unmount; filesRef always current
[usingProvider],
);

Expand Down Expand Up @@ -772,7 +773,7 @@ export const PromptInput = ({
return item;
}),
)
.then((convertedFiles: FileUIPart[]) => {
.then((convertedFiles: Array<FileUIPart>) => {
try {
const result = onSubmit({ text, files: convertedFiles }, event);

Expand Down Expand Up @@ -879,13 +880,9 @@ export const PromptInputTextarea = ({
};

const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
const items = event.clipboardData?.items;

if (!items) {
return;
}
const items = event.clipboardData.items;

const files: File[] = [];
const files: Array<File> = [];

for (const item of items) {
if (item.kind === "file") {
Expand Down Expand Up @@ -1056,8 +1053,8 @@ interface SpeechRecognition extends EventTarget {
continuous: boolean;
interimResults: boolean;
lang: string;
start(): void;
stop(): void;
start: () => void;
stop: () => void;
onstart: ((this: SpeechRecognition, ev: Event) => any) | null;
onend: ((this: SpeechRecognition, ev: Event) => any) | null;
onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null;
Expand All @@ -1071,13 +1068,13 @@ interface SpeechRecognitionEvent extends Event {

type SpeechRecognitionResultList = {
readonly length: number;
item(index: number): SpeechRecognitionResult;
item: (index: number) => SpeechRecognitionResult;
[index: number]: SpeechRecognitionResult;
};

type SpeechRecognitionResult = {
readonly length: number;
item(index: number): SpeechRecognitionAlternative;
item: (index: number) => SpeechRecognitionAlternative;
[index: number]: SpeechRecognitionAlternative;
isFinal: boolean;
};
Expand Down Expand Up @@ -1143,7 +1140,7 @@ export const PromptInputSpeechButton = ({
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
finalTranscript += result[0]?.transcript ?? "";
finalTranscript += result[0].transcript;
}
}

Expand Down
Loading
Loading