Skip to content
Open
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
115 changes: 52 additions & 63 deletions .cursor/rules/convex_rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { query } from "./_generated/server";
import { v } from "convex/values";
export const f = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
// Function body
},
Expand Down Expand Up @@ -71,20 +70,6 @@ export default defineSchema({
)
});
```
- Always use the `v.null()` validator when returning a null value. Below is an example query that returns a null value:
```typescript
import { query } from "./_generated/server";
import { v } from "convex/values";

export const exampleQuery = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This query returns a null value");
return null;
},
});
```
- Here are the valid Convex types along with their respective validators:
Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
| ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand All @@ -103,7 +88,7 @@ Convex Type | TS/JS type | Example Usage | Validator for argument val
- Use `internalQuery`, `internalMutation`, and `internalAction` to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from `./_generated/server`.
- Use `query`, `mutation`, and `action` to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use `query`, `mutation`, or `action` to register sensitive internal functions that should be kept private.
- You CANNOT register a function through the `api` or `internal` objects.
- ALWAYS include argument and return validators for all Convex functions. This includes all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`. If a function doesn't return anything, include `returns: v.null()` as its output validator.
- ALWAYS include argument validators for all Convex functions. This includes all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`.
- If the JavaScript implementation of a Convex function doesn't have a return value, it implicitly returns `null`.

### Function calling
Expand All @@ -117,15 +102,13 @@ Convex Type | TS/JS type | Example Usage | Validator for argument val
```
export const f = query({
args: { name: v.string() },
returns: v.string(),
handler: async (ctx, args) => {
return "Hello " + args.name;
},
});

export const g = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
return null;
Expand Down Expand Up @@ -159,7 +142,7 @@ export const listWithExtraArg = query({
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.filter((q) => q.eq(q.field("author"), args.author))
.withIndex("by_author", (q) => q.eq("author", args.author))
.order("desc")
.paginate(args.paginationOpts);
},
Expand All @@ -169,9 +152,9 @@ Note: `paginationOpts` is an object with the following properties:
- `numItems`: the maximum number of documents to return (the validator is `v.number()`)
- `cursor`: the cursor to use to fetch the next page of documents (the validator is `v.union(v.string(), v.null())`)
- A query that ends in `.paginate()` returns an object that has the following properties:
- page (contains an array of documents that you fetches)
- isDone (a boolean that represents whether or not this is the last page of documents)
- continueCursor (a string that represents the cursor to use to fetch the next page of documents)
- page (contains an array of documents that you fetches)
- isDone (a boolean that represents whether or not this is the last page of documents)
- continueCursor (a string that represents the cursor to use to fetch the next page of documents)


## Validator guidelines
Expand All @@ -194,11 +177,10 @@ import { Doc, Id } from "./_generated/dataModel";

export const exampleQuery = query({
args: { userIds: v.array(v.id("users")) },
returns: v.record(v.id("users"), v.string()),
handler: async (ctx, args) => {
const idToUsername: Record<Id<"users">, string> = {};
for (const userId of args.userIds) {
const user = await ctx.db.get(userId);
const user = await ctx.db.get("users", userId);
if (user) {
idToUsername[user._id] = user.username;
}
Expand All @@ -212,7 +194,6 @@ export const exampleQuery = query({
- Always use `as const` for string literals in discriminated union types.
- When using the `Array` type, make sure to always define your arrays as `const array: Array<T> = [...];`
- When using the `Record` type, make sure to always define your records as `const record: Record<KeyType, ValueType> = {...};`
- Always add `@types/node` to your `package.json` when using any Node.js built-in modules.

## Full text search guidelines
- A query for "10 messages in channel '#general' that best match the query 'hello hi' in their body" would look like:
Expand All @@ -236,19 +217,20 @@ const messages = await ctx.db


## Mutation guidelines
- Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist.
- Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist.
- Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false })`
- Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.patch('tasks', taskId, { completed: true })`

## Action guidelines
- Always add `"use node";` to the top of files containing actions that use Node.js built-in modules.
- Never add `"use node";` to a file that also exports queries or mutations. Only actions can run in the Node.js runtime; queries and mutations must stay in the default Convex runtime. If you need Node.js built-ins alongside queries or mutations, put the action in a separate file.
- `fetch()` is available in the default Convex runtime. You do NOT need `"use node";` just to use `fetch()`.
- Never use `ctx.db` inside of an action. Actions don't have access to the database.
- Below is an example of the syntax for an action:
```ts
import { action } from "./_generated/server";

export const exampleAction = action({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This action does not return anything");
return null;
Expand All @@ -268,7 +250,6 @@ import { internalAction } from "./_generated/server";

const empty = internalAction({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("empty");
},
Expand All @@ -290,7 +271,7 @@ export default crons;
- The `ctx.storage.getUrl()` method returns a signed URL for a given file. It returns `null` if the file doesn't exist.
- Do NOT use the deprecated `ctx.storage.getMetadata` call for loading a file's metadata.

Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`.
Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`.
```
import { query } from "./_generated/server";
import { Id } from "./_generated/dataModel";
Expand All @@ -305,9 +286,8 @@ type FileMetadata = {

export const exampleQuery = query({
args: { fileId: v.id("_storage") },
returns: v.null(),
handler: async (ctx, args) => {
const metadata: FileMetadata | null = await ctx.db.system.get(args.fileId);
const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId);
console.log(metadata);
return null;
},
Expand Down Expand Up @@ -434,8 +414,8 @@ Internal Functions:
"description": "This example shows how to build a chat app without authentication.",
"version": "1.0.0",
"dependencies": {
"convex": "^1.17.4",
"openai": "^4.79.0"
"convex": "^1.31.2",
"openai": "^6.0.0"
},
"devDependencies": {
"typescript": "^5.7.3"
Expand Down Expand Up @@ -480,47 +460,36 @@ import OpenAI from "openai";
import { internal } from "./_generated/api";

/**
* Create a user with a given name.
*/
* Create a user with a given name.
*/
export const createUser = mutation({
args: {
name: v.string(),
},
returns: v.id("users"),
handler: async (ctx, args) => {
return await ctx.db.insert("users", { name: args.name });
},
});

/**
* Create a channel with a given name.
*/
* Create a channel with a given name.
*/
export const createChannel = mutation({
args: {
name: v.string(),
},
returns: v.id("channels"),
handler: async (ctx, args) => {
return await ctx.db.insert("channels", { name: args.name });
},
});

/**
* List the 10 most recent messages from a channel in descending creation order.
*/
* List the 10 most recent messages from a channel in descending creation order.
*/
export const listMessages = query({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
_id: v.id("messages"),
_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
Expand All @@ -532,15 +501,14 @@ export const listMessages = query({
});

/**
* Send a message to a channel and schedule a response from the AI.
*/
* Send a message to a channel and schedule a response from the AI.
*/
export const sendMessage = mutation({
args: {
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
Expand Down Expand Up @@ -568,7 +536,6 @@ export const generateResponse = internalAction({
args: {
channelId: v.id("channels"),
},
returns: v.null(),
handler: async (ctx, args) => {
const context = await ctx.runQuery(internal.index.loadContext, {
channelId: args.channelId,
Expand All @@ -593,12 +560,6 @@ export const loadContext = internalQuery({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
Expand Down Expand Up @@ -634,7 +595,6 @@ export const writeAgentResponse = internalMutation({
channelId: v.id("channels"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("messages", {
channelId: args.channelId,
Expand Down Expand Up @@ -667,9 +627,38 @@ export default defineSchema({
});
```

#### convex/tsconfig.json
```typescript
{
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,

/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
}
```

#### src/App.tsx
```typescript
export default function App() {
return <div>Hello World</div>;
}
```
```
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ template/pnpm-lock.yaml
.env*.local

temp_envars
.hive-tmp
23 changes: 11 additions & 12 deletions app/components/DebugPromptView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useEffect, useCallback, useState } from 'react';
import { JsonView } from 'react-json-view-lite';
import 'react-json-view-lite/dist/index.css';
import type { CoreMessage, FilePart, ToolCallPart, TextPart } from 'ai';
import type { ModelMessage, FilePart, ToolCallPart, TextPart } from 'ai';
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid';
import { ClipboardIcon, ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
import { useDebugPrompt } from '~/lib/hooks/useDebugPrompt';
Expand Down Expand Up @@ -107,17 +107,17 @@ function isToolCallPart(part: unknown): part is ToolCallPart {
return typeof part === 'object' && part !== null && 'type' in part && part.type === 'tool-call';
}

function getMessageCharCount(message: CoreMessage): number {
function getMessageCharCount(message: ModelMessage): number {
if (typeof message.content === 'string') return message.content.length;
if (Array.isArray(message.content)) {
return message.content.reduce((sum, part) => {
if (isTextPart(part)) return sum + part.text.length;
if (isFilePart(part) && typeof part.data === 'string') return sum + part.data.length;
if (isToolCallPart(part)) {
return sum + part.toolName.length + part.toolCallId.length + JSON.stringify(part.args).length;
return sum + part.toolName.length + part.toolCallId.length + JSON.stringify(part.input).length;
}
if (part.type === 'tool-result') {
return sum + part.toolName.length + part.toolCallId.length + JSON.stringify(part.result).length;
return sum + part.toolName.length + part.toolCallId.length + JSON.stringify(part.output).length;
}
return sum;
}, 0);
Expand Down Expand Up @@ -145,7 +145,7 @@ function getPreviewClass(text: string) {
return `preview-${Math.abs(text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0))}`;
}

function findLastAssistantMessage(prompt: CoreMessage[]): string {
function findLastAssistantMessage(prompt: ModelMessage[]): string {
// The last assistant message in a LLM of messages is the response.
// It should generally just be the last message, full stop.
for (let i = prompt.length - 1; i >= 0; i--) {
Expand Down Expand Up @@ -186,7 +186,7 @@ function LlmPromptAndResponseView({ promptAndResponse }: { promptAndResponse: Ll
const totalInputChars = (prompt || []).reduce((sum, msg) => sum + getMessageCharCount(msg), 0);
const totalOutputChars = (completion || []).reduce((sum, msg) => sum + getMessageCharCount(msg), 0);

const getTokenEstimate = (message: CoreMessage) => {
const getTokenEstimate = (message: ModelMessage) => {
const charCount = getMessageCharCount(message);
return estimateTokenCount(charCount, totalInputChars, promptTokensTotal);
};
Expand Down Expand Up @@ -262,12 +262,12 @@ function LlmPromptAndResponseView({ promptAndResponse }: { promptAndResponse: Ll
}

type CoreMessageViewProps = {
message: CoreMessage;
getTokenEstimate?: (message: CoreMessage) => number;
message: ModelMessage;
getTokenEstimate?: (message: ModelMessage) => number;
totalCompletionTokens?: number;
};

function getMessagePreview(content: CoreMessage['content']): string {
function getMessagePreview(content: ModelMessage['content']): string {
if (typeof content === 'string') {
return content;
}
Expand All @@ -288,7 +288,7 @@ function getMessagePreview(content: CoreMessage['content']): string {
}

type MessageContentViewProps = {
content: CoreMessage['content'];
content: ModelMessage['content'];
showRawJson?: boolean;
};

Expand Down Expand Up @@ -320,7 +320,7 @@ function MessageContentView({ content, showRawJson = false }: MessageContentView
const fileData = typeof part.data === 'string' ? part.data : '[Binary Data]';
return (
<div key={idx} className="rounded bg-purple-50 p-2 dark:bg-purple-900/10">
<div className="text-xs font-medium text-purple-500">file: {part.filename || part.mimeType}</div>
<div className="text-xs font-medium text-purple-500">file: {part.filename || part.mediaType}</div>
<div className="whitespace-pre-wrap font-mono text-sm">{fileData}</div>
</div>
);
Expand Down Expand Up @@ -410,7 +410,6 @@ function CoreMessageView({ message, getTokenEstimate, totalCompletionTokens }: C
className={`flex-1 truncate text-sm text-gray-600 dark:text-gray-300 ${getPreviewClass(preview)} before:block before:truncate`}
/>
</button>

<div>
{isExpanded && (
<div className="mt-2">
Expand Down
Loading
Loading