Login
diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts
index f4c3a61..cbad67d 100644
--- a/app/(chat)/actions.ts
+++ b/app/(chat)/actions.ts
@@ -4,6 +4,8 @@ import { CoreMessage, CoreUserMessage, generateText } from "ai";
import { cookies } from "next/headers";
import { customModel } from "@/ai";
+import { createClient } from "@/lib/supabase/server";
+import { getSession } from "@/db/cached-queries";
export async function saveModelId(model: string) {
const cookieStore = await cookies();
@@ -27,3 +29,85 @@ export async function generateTitleFromUserMessage({
return title;
}
+
+export async function getChat(id: string) {
+ try {
+ const user = await getSession();
+ if (!user) return null;
+
+ const supabase = await createClient();
+
+ // Get chat
+ const { data: chat, error: chatError } = await supabase
+ .from('chats')
+ .select('*')
+ .eq('id', id)
+ .eq('user_id', user.id)
+ .single();
+
+ if (chatError || !chat) return null;
+
+ // Get messages
+ const { data: messages, error: messagesError } = await supabase
+ .from('messages')
+ .select('*')
+ .eq('chat_id', id)
+ .order('created_at', { ascending: true });
+
+ if (messagesError) return null;
+
+ return {
+ ...chat,
+ messages: messages || []
+ };
+ } catch (error) {
+ console.error('Error getting chat:', error);
+ return null;
+ }
+}
+
+export async function createChat() {
+ try {
+ const user = await getSession();
+ if (!user) return null;
+
+ const supabase = await createClient();
+
+ const { data: chat, error } = await supabase
+ .from('chats')
+ .insert([
+ {
+ user_id: user.id,
+ title: 'New Chat'
+ }
+ ])
+ .select()
+ .single();
+
+ if (error) throw error;
+ return chat;
+ } catch (error) {
+ console.error('Error creating chat:', error);
+ return null;
+ }
+}
+
+export async function updateChatTitle(id: string, title: string) {
+ try {
+ const user = await getSession();
+ if (!user) return false;
+
+ const supabase = await createClient();
+
+ const { error } = await supabase
+ .from('chats')
+ .update({ title })
+ .eq('id', id)
+ .eq('user_id', user.id);
+
+ return !error;
+ } catch (error) {
+ console.error('Error updating chat title:', error);
+ return false;
+ }
+}
diff --git a/app/(chat)/api/chat/ollama/route.ts b/app/(chat)/api/chat/ollama/route.ts
new file mode 100644
index 0000000..8812ae2
--- /dev/null
+++ b/app/(chat)/api/chat/ollama/route.ts
@@ -0,0 +1,161 @@
+import { NextResponse } from "next/server";
+import { generateUUID } from "@/lib/utils";
+import { StreamingTextResponse } from 'ai';
+import { createClient } from "@/lib/supabase/client";
+import { useModelSettings } from "@/lib/store/model-settings";
+
+export const maxDuration = 300; // Longer timeout for local testing
+
+function logError(context: string, error: any) {
+ console.error('\x1b[31m%s\x1b[0m', `🚨 Error in ${context}:`);
+ console.error('\x1b[31m%s\x1b[0m', error?.message || error);
+ if (error?.stack) {
+ console.error('\x1b[33m%s\x1b[0m', 'Stack trace:');
+ console.error(error.stack);
+ }
+ if (error?.cause) {
+ console.error('\x1b[33m%s\x1b[0m', 'Caused by:');
+ console.error(error.cause);
+ }
+}
+
+export async function POST(req: Request) {
+ const json = await req.json();
+ const { messages, modelId } = json;
+ const chatId = json.id || generateUUID();
+
+ console.log('\x1b[36m%s\x1b[0m', `📝 Processing chat request for model: ${modelId || 'llama2'}`);
+
+ try {
+ // Get model settings from store
+ const modelSettings = useModelSettings.getState().settings;
+ console.log('\x1b[36m%s\x1b[0m', '⚙️ Current model settings:', {
+ temperature: modelSettings.temperature,
+ topK: modelSettings.topK,
+ topP: modelSettings.topP,
+ repeatPenalty: modelSettings.repeatPenalty,
+ });
+
+ // Add system message to the beginning of the messages array
+ const systemMessage = {
+ role: 'system',
+ content: modelSettings.systemPrompt
+ };
+
+ // Make request to local Ollama instance
+ console.log('\x1b[36m%s\x1b[0m', '🔄 Making request to Ollama...');
+ const response = await fetch('http://localhost:11434/api/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ model: modelId || 'llama2',
+ messages: [systemMessage, ...messages].map((message: any) => ({
+ role: message.role === 'user' ? 'user' : 'assistant',
+ content: message.content,
+ })),
+ stream: true,
+ options: {
+ temperature: modelSettings.temperature,
+ num_predict: modelSettings.numPredict,
+ top_k: modelSettings.topK,
+ top_p: modelSettings.topP,
+ repeat_penalty: modelSettings.repeatPenalty,
+ stop: modelSettings.stop,
+ },
+ }),
+ }).catch(error => {
+ logError('Ollama API request', error);
+ throw new Error('Failed to connect to Ollama. Is it running?', { cause: error });
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text().catch(() => 'No error details available');
+ logError('Ollama API response', new Error(`HTTP ${response.status}: ${errorText}`));
+ throw new Error(`Ollama API error: ${response.statusText}`);
+ }
+
+ // Create a TransformStream to handle the response
+ const { readable, writable } = new TransformStream();
+ const writer = writable.getWriter();
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ // Process the stream
+ const processStream = async () => {
+ const reader = response.body?.getReader();
+ if (!reader) {
+ logError('Stream processing', new Error('No response body available'));
+ throw new Error('No response body');
+ }
+
+ try {
+ let buffer = '';
+ let currentMessage = '';
+ let messageCount = 0;
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const chunk = decoder.decode(value);
+ buffer += chunk;
+
+ // Process complete JSON objects
+ const lines = buffer.split('\n').filter(Boolean);
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
+
+ for (const line of lines) {
+ try {
+ const { message, done: responseDone, error } = JSON.parse(line);
+
+ if (error) {
+ logError('Ollama response', new Error(error));
+ continue;
+ }
+
+ if (message?.content) {
+ currentMessage += message.content;
+ messageCount++;
+ // Forward the model's response
+ writer.write(encoder.encode(message.content));
+ }
+
+ if (responseDone) {
+ console.log('\x1b[32m%s\x1b[0m', `✅ Response complete - Processed ${messageCount} chunks`);
+ }
+ } catch (e) {
+ logError('JSON parsing', e);
+ }
+ }
+ }
+ } catch (error) {
+ logError('Stream processing', error);
+ throw error;
+ } finally {
+ writer.close();
+ }
+ };
+
+ // Start processing the stream
+ processStream();
+
+ // Return the transformed stream
+ return new StreamingTextResponse(readable);
+ } catch (error: any) {
+ logError('Main process', error);
+ return new Response(
+ JSON.stringify({
+ error: error.message || 'An error occurred during the Ollama API request',
+ details: error.cause?.message || error.cause,
+ }),
+ {
+ status: 500,
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts
index d3f31aa..2084ff2 100644
--- a/app/(chat)/api/chat/route.ts
+++ b/app/(chat)/api/chat/route.ts
@@ -1,837 +1,268 @@
-import {
- CoreMessage,
- Message,
- StreamData,
- convertToCoreMessages,
- streamObject,
- streamText,
-} from "ai";
-import { ethers } from "ethers";
-import { z } from "zod";
-
-import { customModel } from "@/ai";
-import { models } from "@/ai/models";
-import { blocksPrompt, regularPrompt, systemPrompt } from "@/ai/prompts";
-import { getChatById, getDocumentById, getSession } from "@/db/cached-queries";
-import {
- saveChat,
- saveDocument,
- saveMessages,
- saveSuggestions,
- deleteChatById,
-} from "@/db/mutations";
-import { createClient } from "@/lib/supabase/server";
-import { MessageRole } from "@/lib/supabase/types";
-import {
- generateUUID,
- getMostRecentUserMessage,
- sanitizeResponseMessages,
-} from "@/lib/utils";
-import { searchDuckDuckGo, searchOpenSearch } from "@/lib/search/search-utils";
-import { FEATURES } from "@/lib/features";
-import { useWalletState } from "@/hooks/useWalletState";
-import { getServerWalletState } from "@/hooks/useServerWalletState";
-import { kv } from "@vercel/kv";
-
-import { useAccount, useBalance, useChainId } from "wagmi";
-
-import { generateTitleFromUserMessage } from "../../actions";
-
-export const maxDuration = 60;
-
-interface WeatherParams {
- latitude: number;
- longitude: number;
-}
-
-interface CreateDocumentParams {
- title: string;
- modelId: string;
-}
-
-interface UpdateDocumentParams {
- id: string;
- description: string;
- modelId: string;
-}
-
-interface RequestSuggestionsParams {
- documentId: string;
- modelId: string;
-}
-
-interface WalletStateParams {
- address?: string;
- chainId?: number;
-}
-
-type AllowedTools =
- | "createDocument"
- | "updateDocument"
- | "requestSuggestions"
- | "getWeather"
- | "getWalletBalance"
- | "checkWalletState"
- | "webSearch";
-
-const blocksTools: AllowedTools[] = [
- "createDocument",
- "updateDocument",
- "requestSuggestions",
-];
-
-const weatherTools: AllowedTools[] = ["getWeather"];
-
-const allTools: AllowedTools[] = [
- ...blocksTools,
- ...weatherTools,
- "getWalletBalance" as AllowedTools,
- "checkWalletState" as AllowedTools,
- ...(FEATURES.WEB_SEARCH ? ["webSearch" as AllowedTools] : []),
-];
-
-async function getUser() {
- const supabase = await createClient();
- const {
- data: { user },
- error,
- } = await supabase.auth.getUser();
-
- if (error || !user) {
- throw new Error("Unauthorized");
- }
-
- return user;
-}
+import { OpenAIStream, StreamingTextResponse } from 'ai';
+import { Configuration, OpenAIApi } from 'openai-edge';
+import { headers } from 'next/headers';
+import { kv } from '@vercel/kv';
+import { getCoinPrice, getCoinMarketChart, searchCoins } from '@/app/lib/services/coingecko';
+import { functions, errorMessages, responseTemplates } from '@/ai/prompts';
+import { WeatherParams, CryptoPriceParams } from '@/app/lib/types/functions';
+
+export const runtime = 'edge';
+
+const OPENAI_API_KEY = process.env.NEXT_PUBLIC_OPENAI_API_KEY!;
+
+// Available models configuration
+const MODELS = {
+ 'gpt-4': 'gpt-4',
+ 'gpt-4-turbo': 'gpt-4-0125-preview',
+ 'gpt-4-1106': 'gpt-4-1106-preview',
+ 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
+} as const;
+
+// Rate limiting configuration
+const RATE_LIMIT = {
+ WINDOW_MS: 60000, // 1 minute
+ MAX_REQUESTS: 100, // Maximum requests per window
+ MAX_TOKENS: 100000 // Maximum tokens per window
+};
-// Add helper function to format message content for database storage
-function formatMessageContent(message: CoreMessage): string {
- // For user messages, store as plain text
- if (message.role === "user") {
- return typeof message.content === "string"
- ? message.content
- : JSON.stringify(message.content);
+// Logger function
+const logger = {
+ info: (message: string, data?: any) => {
+ console.log(`[INFO] ${message}`, data ? JSON.stringify(data) : '');
+ },
+ error: (message: string, error: any) => {
+ console.error(`[ERROR] ${message}:`, error);
+ console.error('Stack trace:', error.stack);
+ },
+ warn: (message: string, data?: any) => {
+ console.warn(`[WARN] ${message}`, data ? JSON.stringify(data) : '');
}
+};
- // For tool messages, format as array of tool results
- if (message.role === "tool") {
- return JSON.stringify(
- message.content.map((content) => ({
- type: content.type || "tool-result",
- toolCallId: content.toolCallId,
- toolName: content.toolName,
- result: content.result,
- })),
- );
- }
+async function checkRateLimit(identifier: string) {
+ try {
+ const now = Date.now();
+ const windowStart = now - RATE_LIMIT.WINDOW_MS;
+
+ const usage = await kv.get<{ requests: number; tokens: number; timestamp: number }>(
+ `ratelimit:${identifier}`
+ ) || { requests: 0, tokens: 0, timestamp: now };
+
+ if (usage.timestamp < windowStart) {
+ usage.requests = 0;
+ usage.tokens = 0;
+ usage.timestamp = now;
+ }
- // For assistant messages, format as array of text and tool calls
- if (message.role === "assistant") {
- if (typeof message.content === "string") {
- return JSON.stringify([{ type: "text", text: message.content }]);
+ if (usage.requests >= RATE_LIMIT.MAX_REQUESTS) {
+ logger.warn('Rate limit exceeded', { identifier, usage });
+ throw new Error('Rate limit exceeded - too many requests');
+ }
+ if (usage.tokens >= RATE_LIMIT.MAX_TOKENS) {
+ logger.warn('Token limit exceeded', { identifier, usage });
+ throw new Error('Rate limit exceeded - token limit reached');
}
- return JSON.stringify(
- message.content.map((content) => {
- if (content.type === "text") {
- return {
- type: "text",
- text: content.text,
- };
- }
- return {
- type: "tool-call",
- toolCallId: content.toolCallId,
- toolName: content.toolName,
- args: content.args,
- };
- }),
- );
+ usage.requests++;
+ await kv.set(`ratelimit:${identifier}`, usage, { ex: 60 });
+ return usage;
+ } catch (error) {
+ logger.error('Rate limit check failed', error);
+ throw error;
}
-
- return "";
-}
-
-// Add type for wallet balance parameters
-interface WalletBalanceParams {
- address: string;
- network?: string;
-}
-
-// Add interface for wallet message content
-interface WalletMessageContent {
- text: string;
- walletAddress?: string;
- chainId?: number;
- network?: string;
- isWalletConnected?: boolean;
- attachments?: Array<{
- url: string;
- name: string;
- type: string;
- }>;
}
-// Add interface for wallet state
-interface WalletState {
- address: string | null;
- isConnected: boolean;
- chainId?: number;
- networkInfo?: {
- name: string;
- id: number;
- };
- isCorrectNetwork: boolean;
- balances?: {
- eth?: string;
- usdc?: string;
- };
- lastUpdated?: string;
+// Function implementations
+async function getCurrentWeather({ location, unit = 'celsius' }: WeatherParams) {
+ try {
+ logger.info('Fetching weather data', { location, unit });
+ // This would normally call a weather API
+ // For demo purposes, returning mock data
+ return {
+ location,
+ temperature: 22,
+ unit,
+ condition: 'sunny',
+ };
+ } catch (error) {
+ logger.error('Weather fetch failed', error);
+ throw error;
+ }
}
-const WALLET_KEY_PREFIX = "wallet-state:";
-
-// Update the tools object to properly handle tool results
-const tools = {
- getWeather: {
- description: "Get the current weather at a location",
- parameters: z.object({
- latitude: z.number(),
- longitude: z.number(),
- }),
- execute: async ({ latitude, longitude }: WeatherParams) => {
- const response = await fetch(
- `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`,
- );
-
- const weatherData = await response.json();
- return weatherData;
- },
- },
- createDocument: {
- description: "Create a document for a writing activity",
- parameters: z.object({
- title: z.string(),
- }),
- execute: async (params: CreateDocumentParams) => {
- const id = generateUUID();
- let draftText: string = "";
- const data = new StreamData();
-
- data.append({ type: "id", content: id });
- data.append({ type: "title", content: params.title });
- data.append({ type: "clear", content: "" });
-
- const { fullStream } = await streamText({
- model: customModel(params.modelId),
- system:
- "Write about the given topic. Markdown is supported. Use headings wherever appropriate.",
- prompt: params.title,
- });
-
- for await (const delta of fullStream) {
- const { type } = delta;
-
- if (type === "text-delta") {
- draftText += delta.textDelta;
- // Stream content updates in real-time
- data.append({
- type: "text-delta",
- content: delta.textDelta,
- });
- }
- }
-
- data.append({ type: "finish", content: "" });
-
- const currentUser = await getUser();
- if (currentUser?.id) {
- await saveDocument({
- id,
- title: params.title,
- content: draftText,
- userId: currentUser.id,
- });
- }
-
- return {
- id,
- title: params.title,
- content: `A document was created and is now visible to the user.`,
- };
- },
- },
- updateDocument: {
- description: "Update a document with the given description",
- parameters: z.object({
- id: z.string(),
- description: z.string(),
- }),
- execute: async (params: UpdateDocumentParams) => {
- const document = await getDocumentById(params.id);
- const data = new StreamData();
-
- if (!document) {
- return { error: "Document not found" };
- }
-
- const { content: currentContent } = document;
- let draftText: string = "";
-
- data.append({
- type: "clear",
- content: document.title,
- });
-
- const { fullStream } = await streamText({
- model: customModel(params.modelId),
- system:
- "You are a helpful writing assistant. Based on the description, please update the piece of writing.",
- experimental_providerMetadata: {
- openai: {
- prediction: {
- type: "content",
- content: currentContent || "",
- },
- },
- },
- messages: [
- { role: "user", content: params.description },
- { role: "user", content: currentContent || "" },
- ],
- });
-
- for await (const delta of fullStream) {
- const { type } = delta;
+async function getCryptoPriceWithRetry({ symbol, currency = 'USD' }: CryptoPriceParams) {
+ const maxRetries = 3;
+ let lastError;
- if (type === "text-delta") {
- const { textDelta } = delta;
- draftText += textDelta;
- data.append({
- type: "text-delta",
- content: textDelta,
- });
- }
- }
-
- data.append({ type: "finish", content: "" });
-
- const currentUser = await getUser();
- if (currentUser?.id) {
- await saveDocument({
- id: params.id,
- title: document.title,
- content: draftText,
- userId: currentUser.id,
- });
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ logger.info('Fetching crypto price', { symbol, currency, attempt });
+
+ // Search for the coin ID
+ const searchResults = await searchCoins(symbol);
+ if (!searchResults.length) {
+ throw new Error(errorMessages.cryptoNotFound);
}
- return {
- id: params.id,
- title: document.title,
- content: "The document has been updated successfully.",
+ const coin = searchResults[0];
+
+ // Get current price
+ const price = await getCoinPrice(coin.id, currency.toLowerCase());
+
+ // Get historical data for chart
+ const chartData = await getCoinMarketChart(coin.id, currency.toLowerCase(), 7);
+
+ // Format the data for the chart
+ const formattedChartData = {
+ timestamps: chartData.prices.map(([timestamp]) => timestamp),
+ prices: chartData.prices.map(([, price]) => price),
};
- },
- },
- requestSuggestions: {
- description: "Request suggestions for a document",
- parameters: z.object({
- documentId: z.string(),
- }),
- execute: async (params: RequestSuggestionsParams) => {
- const document = await getDocumentById(params.documentId);
- const data = new StreamData();
- const suggestions: Array<{
- originalText: string;
- suggestedText: string;
- description: string;
- id: string;
- documentId: string;
- isResolved: boolean;
- }> = [];
-
- if (!document || !document.content) {
- return { error: "Document not found" };
- }
-
- const { elementStream } = await streamObject({
- model: customModel(params.modelId),
- system:
- "You are a help writing assistant. Given a piece of writing, please offer suggestions to improve the piece of writing and describe the change. It is very important for the edits to contain full sentences instead of just words. Max 5 suggestions.",
- prompt: document.content,
- output: "array",
- schema: z.object({
- originalSentence: z.string().describe("The original sentence"),
- suggestedSentence: z.string().describe("The suggested sentence"),
- description: z.string().describe("The description of the suggestion"),
- }),
- });
-
- for await (const element of elementStream) {
- const suggestion = {
- originalText: element.originalSentence,
- suggestedText: element.suggestedSentence,
- description: element.description,
- id: generateUUID(),
- documentId: params.documentId,
- isResolved: false,
- };
-
- data.append({
- type: "suggestion",
- content: suggestion,
- });
- suggestions.push(suggestion);
- }
-
- const currentUser = await getUser();
- if (currentUser?.id) {
- await saveSuggestions({
- suggestions: suggestions.map((suggestion) => ({
- ...suggestion,
- userId: currentUser.id,
- createdAt: new Date(),
- documentCreatedAt: document.created_at,
- })),
- });
- }
- return {
- id: params.documentId,
- title: document.title,
- message: "Suggestions have been added to the document",
+ const response = {
+ symbol: coin.symbol.toUpperCase(),
+ name: coin.name,
+ price,
+ currency,
+ chartData: formattedChartData,
+ thumbnail: coin.thumb,
+ marketCapRank: coin.market_cap_rank,
};
- },
- },
- getWalletBalance: {
- description: "Get the balance of the connected wallet",
- parameters: z.object({
- address: z.string().describe("The wallet address to check"),
- chainId: z.number().describe("The chain ID of the connected wallet"),
- }),
- execute: async ({
- address,
- chainId,
- }: {
- address: string;
- chainId: number;
- }) => {
- try {
- const walletState = await kv.get
(
- `${WALLET_KEY_PREFIX}${address}`,
- );
-
- if (!walletState) {
- return {
- type: "tool-result",
- result: {
- error: "No wallet state found",
- details: "Please connect your wallet first",
- },
- };
- }
-
- // Validate supported network
- if (![8453, 84532].includes(chainId)) {
- return {
- type: "tool-result",
- result: {
- error: `Unsupported chain ID: ${chainId}`,
- details: "Please connect to Base Mainnet or Base Sepolia.",
- },
- };
- }
-
- const networkName = chainId === 8453 ? "Base Mainnet" : "Base Sepolia";
- const usdcAddress =
- chainId === 8453
- ? "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" // Base Mainnet USDC
- : "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; // Base Sepolia USDC
-
- // Create provider based on network
- const provider = new ethers.JsonRpcProvider(
- chainId === 8453
- ? "https://mainnet.base.org"
- : "https://sepolia.base.org",
- );
-
- // Get ETH balance
- const ethBalance = await provider.getBalance(address);
-
- // Create USDC contract instance
- const usdcContract = new ethers.Contract(
- usdcAddress,
- ["function balanceOf(address) view returns (uint256)"],
- provider,
- );
-
- // Get USDC balance
- const usdcBalance = await usdcContract.balanceOf(address);
-
- // Update wallet state with new balances
- const updatedState = {
- ...walletState,
- balances: {
- eth: ethers.formatEther(ethBalance),
- usdc: ethers.formatUnits(usdcBalance, 6),
- },
- lastUpdated: new Date().toISOString(),
- };
-
- // Save updated state
- await kv.set(
- `${WALLET_KEY_PREFIX}${address}`,
- JSON.stringify(updatedState),
- );
-
- return {
- type: "tool-result",
- result: {
- address,
- network: networkName,
- chainId,
- balances: updatedState.balances,
- timestamp: updatedState.lastUpdated,
- },
- };
- } catch (error) {
- console.error("Error fetching wallet balance:", error);
- return {
- type: "tool-result",
- result: {
- error: "Failed to fetch wallet balance",
- details: error instanceof Error ? error.message : "Unknown error",
- },
- };
- }
- },
- },
- checkWalletState: {
- description: "Check the current state of the connected wallet",
- parameters: z.object({
- address: z.string().optional().describe("The wallet address to check"),
- chainId: z.number().optional().describe("The chain ID to check"),
- }),
- execute: async ({ address }: WalletStateParams) => {
- try {
- const walletState = address
- ? await kv.get(`${WALLET_KEY_PREFIX}${address}`)
- : null;
- return {
- type: "tool-result",
- result: {
- isConnected: !!walletState?.address,
- address: walletState?.address || null,
- chainId: walletState?.chainId || null,
- network: walletState?.networkInfo?.name || "Unknown Network",
- isSupported: walletState?.isCorrectNetwork || false,
- supportedNetworks: [
- { name: "Base Mainnet", chainId: 8453 },
- { name: "Base Sepolia", chainId: 84532 },
- ],
- timestamp: new Date().toISOString(),
- },
- };
- } catch (error) {
- console.error("Error checking wallet state:", error);
- return {
- type: "tool-result",
- result: {
- error: "Failed to check wallet state",
- details: error instanceof Error ? error.message : "Unknown error",
- },
- };
+ logger.info('Crypto price fetched successfully', { symbol, currency });
+ return response;
+ } catch (error: any) {
+ lastError = error;
+ logger.error(`Crypto price fetch attempt ${attempt} failed`, error);
+
+ if (error.message.includes('Rate limit exceeded')) {
+ // Wait longer for rate limit errors
+ await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
+ } else if (attempt < maxRetries) {
+ // Wait less for other errors
+ await new Promise(resolve => setTimeout(resolve, 1000));
}
- },
- },
- ...(FEATURES.WEB_SEARCH
- ? {
- webSearch: {
- description: "Search the web using DuckDuckGo",
- parameters: z.object({
- query: z.string().describe("The search query"),
- searchType: z
- .enum(["duckduckgo", "opensearch"])
- .describe("The search engine to use"),
- }),
- execute: async ({
- query,
- searchType,
- }: {
- query: string;
- searchType: "duckduckgo" | "opensearch";
- }) => {
- try {
- let results;
- if (searchType === "duckduckgo") {
- results = await searchDuckDuckGo(query);
- } else {
- results = await searchOpenSearch(query);
- }
-
- return {
- type: "tool-result",
- result: {
- searchEngine: searchType,
- query,
- results,
- timestamp: new Date().toISOString(),
- },
- };
- } catch (error) {
- return {
- type: "tool-result",
- result: {
- error: "Search failed",
- details:
- error instanceof Error ? error.message : "Unknown error",
- },
- };
- }
- },
- },
- }
- : {}),
-};
-
-export async function POST(request: Request) {
- try {
- const {
- id,
- messages,
- modelId,
- }: { id: string; messages: Array; modelId: string } =
- await request.json();
-
- const user = await getUser();
-
- if (!user) {
- return new Response("Unauthorized", { status: 401 });
- }
-
- const model = models.find((model) => model.id === modelId);
-
- if (!model) {
- return new Response("Model not found", { status: 404 });
}
+ }
- const coreMessages = convertToCoreMessages(messages);
- const userMessage = getMostRecentUserMessage(coreMessages);
-
- if (!userMessage) {
- return new Response("No user message found", { status: 400 });
- }
+ throw lastError;
+}
- // Parse the message content and create context
- let walletInfo: WalletMessageContent = { text: "" };
- try {
- if (typeof userMessage.content === "string") {
- try {
- walletInfo = JSON.parse(userMessage.content);
- } catch {
- walletInfo = { text: userMessage.content };
- }
- }
- } catch (e) {
- console.error("Error processing message content:", e);
- walletInfo = {
- text:
- typeof userMessage.content === "string" ? userMessage.content : "",
- };
+export async function POST(req: Request) {
+ try {
+ const { messages, modelId = 'gpt-4o-mini' } = await req.json();
+
+ if (!messages || !Array.isArray(messages)) {
+ throw new Error('Invalid messages format');
}
- // Create messages with enhanced wallet context
- const messagesWithContext = coreMessages.map((msg) => {
- if (msg.role === "user" && msg === userMessage) {
- const baseMessage = {
- ...msg,
- content:
- walletInfo.text ||
- (typeof msg.content === "string"
- ? msg.content
- : JSON.stringify(msg.content)),
- };
-
- if (walletInfo.walletAddress && walletInfo.chainId !== undefined) {
- return {
- ...baseMessage,
- walletAddress: walletInfo.walletAddress,
- chainId: walletInfo.chainId,
- isWalletConnected: true,
- lastChecked: new Date().toISOString(),
- };
- }
-
- return {
- ...baseMessage,
- isWalletConnected: false,
- lastChecked: new Date().toISOString(),
- };
- }
- return msg;
+ logger.info('Processing chat request', { modelId });
+
+ // Get client identifier (IP address or user ID)
+ const headersList = headers();
+ const forwardedFor = headersList.get('x-forwarded-for');
+ const identifier = forwardedFor || 'anonymous';
+
+ // Check rate limits
+ await checkRateLimit(identifier);
+
+ // Get the actual model identifier
+ const actualModelId = MODELS[modelId as keyof typeof MODELS] || MODELS['gpt-4o-mini'];
+
+ // Create OpenAI client with the provided API key
+ const config = new Configuration({ apiKey: OPENAI_API_KEY });
+ const openai = new OpenAIApi(config);
+
+ // Make the request to OpenAI with model configuration and function calling
+ const response = await openai.createChatCompletion({
+ model: actualModelId,
+ messages: messages.map((message: any) => ({
+ role: message.role,
+ content: message.content,
+ })),
+ functions,
+ function_call: 'auto',
+ max_tokens: 1000,
+ temperature: 0.7,
+ stream: true,
});
- // Initialize streaming data
- const streamingData = new StreamData();
+ // Handle function calls if present
+ const responseBody = await response.json();
+ if (responseBody.choices?.[0]?.message?.function_call) {
+ const functionCall = responseBody.choices[0].message.function_call;
+ let functionResponse;
- try {
- // Try to get existing chat
- const chat = await getChatById(id);
+ logger.info('Function call detected', { function: functionCall.name });
- // If chat doesn't exist, create it
- if (!chat) {
- const title = await generateTitleFromUserMessage({
- message: userMessage as unknown as { role: "user"; content: string },
- });
- try {
- await saveChat({ id, userId: user.id, title });
- } catch (error) {
- // Ignore duplicate chat error, continue with message saving
- if (
- !(
- error instanceof Error &&
- error.message === "Chat ID already exists"
- )
- ) {
- throw error;
- }
+ try {
+ // Execute the appropriate function
+ switch (functionCall.name) {
+ case 'get_current_weather':
+ functionResponse = await getCurrentWeather(JSON.parse(functionCall.arguments));
+ break;
+ case 'get_crypto_price':
+ functionResponse = await getCryptoPriceWithRetry(JSON.parse(functionCall.arguments));
+ break;
+ default:
+ throw new Error(`Unknown function: ${functionCall.name}`);
}
- } else if (chat.user_id !== user.id) {
- return new Response("Unauthorized", { status: 401 });
- }
- // Save the user message
- await saveMessages({
- chatId: id,
- messages: [
+ // Add the function response to messages and make another request
+ const newMessages = [
+ ...messages,
{
- id: generateUUID(),
- chat_id: id,
- role: userMessage.role as MessageRole,
- content: formatMessageContent(userMessage),
- created_at: new Date().toISOString(),
- },
- ],
- });
-
- // Process the message with AI
- const result = await streamText({
- model: customModel(model.apiIdentifier),
- system: systemPrompt,
- messages: messagesWithContext,
- maxSteps: 5,
- experimental_activeTools: allTools,
- tools: {
- ...tools,
- createDocument: {
- ...tools.createDocument,
- execute: (params) =>
- tools.createDocument.execute({
- ...params,
- modelId: model.apiIdentifier,
- }),
- },
- updateDocument: {
- ...tools.updateDocument,
- execute: (params) =>
- tools.updateDocument.execute({
- ...params,
- modelId: model.apiIdentifier,
- }),
+ role: 'assistant',
+ content: null,
+ function_call: functionCall,
},
- requestSuggestions: {
- ...tools.requestSuggestions,
- execute: (params) =>
- tools.requestSuggestions.execute({
- ...params,
- modelId: model.apiIdentifier,
- }),
+ {
+ role: 'function',
+ name: functionCall.name,
+ content: JSON.stringify(functionResponse),
},
- },
- onFinish: async ({ responseMessages }) => {
- if (user && user.id) {
- try {
- const responseMessagesWithoutIncompleteToolCalls =
- sanitizeResponseMessages(responseMessages);
-
- await saveMessages({
- chatId: id,
- messages: responseMessagesWithoutIncompleteToolCalls.map(
- (message) => {
- const messageId = generateUUID();
- if (message.role === "assistant") {
- streamingData.appendMessageAnnotation({
- messageIdFromServer: messageId,
- });
- }
- return {
- id: messageId,
- chat_id: id,
- role: message.role as MessageRole,
- content: formatMessageContent(message),
- created_at: new Date().toISOString(),
- };
- },
- ),
- });
- } catch (error) {
- console.error("Failed to save chat:", error);
- }
- }
- streamingData.close();
- },
- experimental_telemetry: {
- isEnabled: true,
- functionId: "stream-text",
- },
- });
-
- return result.toDataStreamResponse({
- data: streamingData,
- });
- } catch (error) {
- console.error("Error in chat route:", error);
- return new Response(JSON.stringify({ error: "Internal server error" }), {
- status: 500,
- });
- }
- } catch (error) {
- console.error("Error parsing request:", error);
- return new Response(JSON.stringify({ error: "Invalid request" }), {
- status: 400,
- });
- }
-}
-
-export async function DELETE(request: Request) {
- const { searchParams } = new URL(request.url);
- const id = searchParams.get("id");
-
- if (!id) {
- return new Response("Not Found", { status: 404 });
- }
-
- const user = await getUser();
-
- try {
- const chat = await getChatById(id);
+ ];
- if (!chat) {
- return new Response("Chat not found", { status: 404 });
- }
+ const secondResponse = await openai.createChatCompletion({
+ model: actualModelId,
+ messages: newMessages,
+ stream: true,
+ });
- if (chat.user_id !== user.id) {
- return new Response("Unauthorized", { status: 401 });
+ // Create a stream from the second response
+ const stream = OpenAIStream(secondResponse);
+ return new StreamingTextResponse(stream);
+ } catch (error: any) {
+ logger.error('Function execution failed', error);
+
+ // Return a friendly error message
+ const errorMessage = error.message.includes('Rate limit exceeded')
+ ? errorMessages.rateLimitExceeded
+ : error.message.includes('not found')
+ ? errorMessages.cryptoNotFound
+ : errorMessages.generalError;
+
+ return new Response(
+ JSON.stringify({ error: errorMessage }),
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
+ );
+ }
}
- await deleteChatById(id, user.id);
-
- return new Response("Chat deleted", { status: 200 });
- } catch (error) {
- console.error("Error deleting chat:", error);
- return new Response("An error occurred while processing your request", {
- status: 500,
- });
+ // If no function call, just stream the response
+ const stream = OpenAIStream(response);
+ return new StreamingTextResponse(stream);
+ } catch (error: any) {
+ logger.error('Chat request failed', error);
+
+ return new Response(
+ JSON.stringify({
+ error: error.message || 'Internal Server Error',
+ details: error.cause?.toString()
+ }),
+ {
+ status: error.status || 500,
+ headers: { 'Content-Type': 'application/json' }
+ }
+ );
}
}
diff --git a/app/(chat)/api/chat/stream/route.ts b/app/(chat)/api/chat/stream/route.ts
new file mode 100644
index 0000000..709a62d
--- /dev/null
+++ b/app/(chat)/api/chat/stream/route.ts
@@ -0,0 +1,71 @@
+import { getSession } from "@/db/cached-queries";
+import { createClient } from "@/lib/supabase/server";
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const chatId = searchParams.get('chatId');
+ const user = await getSession();
+
+ if (!user || !chatId) {
+ return Response.json("Unauthorized!", { status: 401 });
+ }
+
+ // Set up SSE headers
+ const headers = new Headers({
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ });
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ try {
+ const supabase = await createClient();
+
+ // Subscribe to realtime changes using channel
+ const channel = supabase.channel(`messages:${chatId}`)
+ .on(
+ 'postgres_changes',
+ {
+ event: 'INSERT',
+ schema: 'public',
+ table: 'messages',
+ filter: `chat_id=eq.${chatId}`,
+ },
+ (payload) => {
+ if (payload.new && payload.new.type === 'intermediate') {
+ const data = JSON.stringify({
+ type: 'intermediate',
+ content: payload.new.content,
+ data: payload.new.data
+ });
+ controller.enqueue(`data: ${data}\n\n`);
+ }
+ }
+ );
+
+ // Subscribe to the channel
+ await channel.subscribe((status) => {
+ if (status === 'SUBSCRIBED') {
+ // Send initial connection success message
+ const data = JSON.stringify({
+ type: 'connection',
+ status: 'connected'
+ });
+ controller.enqueue(`data: ${data}\n\n`);
+ }
+ });
+
+ // Clean up subscription when client disconnects
+ return () => {
+ channel.unsubscribe();
+ };
+ } catch (error) {
+ console.error('Streaming error:', error);
+ controller.error(error);
+ }
+ }
+ });
+
+ return new Response(stream, { headers });
+}
\ No newline at end of file
diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx
index 134adce..2c7ca32 100644
--- a/app/(chat)/chat/[id]/page.tsx
+++ b/app/(chat)/chat/[id]/page.tsx
@@ -1,49 +1,28 @@
-import { cookies } from "next/headers";
-import { notFound } from "next/navigation";
-
-import { DEFAULT_MODEL_NAME, models } from "@/ai/models";
-import { Chat as PreviewChat } from "@/components/custom/chat";
-import {
- getChatById,
- getMessagesByChatId,
- getSession,
-} from "@/db/cached-queries";
-import { convertToUIMessages } from "@/lib/utils";
-
-export default async function Page(props: { params: Promise }) {
- const params = await props.params;
- const { id } = params;
- const chat = await getChatById(id);
+import { Chat } from "@/components/custom/chat";
+import { getChat } from "@/app/(chat)/actions";
+import { redirect } from "next/navigation";
+
+interface ChatPageProps {
+ params: {
+ id: string;
+ };
+ searchParams: {
+ model?: string;
+ };
+}
+export default async function ChatPage({ params, searchParams }: ChatPageProps) {
+ const chat = await getChat(params.id);
+
if (!chat) {
- notFound();
- }
-
- const user = await getSession();
-
- if (!user) {
- return notFound();
+ redirect("/");
}
- if (user.id !== chat.user_id) {
- return notFound();
- }
-
- const messagesFromDb = await getMessagesByChatId(id);
-
- const cookieStore = await cookies();
- const modelIdFromCookie = cookieStore.get("model-id")?.value;
- const selectedModelId =
- models.find((model) => model.id === modelIdFromCookie)?.id ||
- DEFAULT_MODEL_NAME;
-
- console.log(convertToUIMessages(messagesFromDb));
-
return (
-
);
}
diff --git a/app/api/models/route.ts b/app/api/models/route.ts
new file mode 100644
index 0000000..168e159
--- /dev/null
+++ b/app/api/models/route.ts
@@ -0,0 +1,71 @@
+import { exec } from 'child_process';
+import { promisify } from 'util';
+
+const execAsync = promisify(exec);
+
+interface OllamaModel {
+ name: string;
+ size: string;
+ modified: string;
+}
+
+function parseSize(size: string): number {
+ const match = size.match(/^([\d.]+)\s*([KMGT]B)$/i);
+ if (!match) return 0;
+
+ const [, num, unit] = match;
+ const multipliers = { KB: 1, MB: 1024, GB: 1024 * 1024, TB: 1024 * 1024 * 1024 };
+ return parseFloat(num) * (multipliers[unit as keyof typeof multipliers] || 1);
+}
+
+export async function GET() {
+ try {
+ // Only fetch Ollama models in development
+ if (process.env.NODE_ENV === 'development') {
+ const { stdout } = await execAsync('ollama list');
+
+ // Parse the output to get model names and sizes
+ const models: OllamaModel[] = stdout
+ .split('\n')
+ .slice(1) // Skip header row
+ .filter(Boolean)
+ .map(line => {
+ const [name, , size, , modified] = line.split(/\s+/);
+ return { name, size, modified };
+ })
+ .filter(model => model.name.toLowerCase().includes('llama'))
+ .sort((a, b) => parseSize(b.size) - parseSize(a.size)) // Sort by size, largest first
+ .slice(0, 2); // Take only top 2 models
+
+ return Response.json({
+ models: [
+ // Default OpenAI models
+ { id: 'gpt-4', name: 'GPT-4', provider: 'openai' },
+ { id: 'gpt-4o-mini', name: 'GPT-4O Mini', provider: 'openai' },
+ { id: 'gpt-4o', name: 'GPT-4O', provider: 'openai' },
+ { id: 'gpt-3.5-turbo', name: 'GPT-3.5', provider: 'openai' },
+ // Add top 2 local Llama models
+ ...models.map(model => ({
+ id: model.name,
+ name: model.name.split(':')[0],
+ provider: 'ollama' as const,
+ size: model.size
+ }))
+ ]
+ });
+ }
+
+ // In production, return only OpenAI models
+ return Response.json({
+ models: [
+ { id: 'gpt-4', name: 'GPT-4', provider: 'openai' },
+ { id: 'gpt-4o-mini', name: 'GPT-4O Mini', provider: 'openai' },
+ { id: 'gpt-4o', name: 'GPT-4O', provider: 'openai' },
+ { id: 'gpt-3.5-turbo', name: 'GPT-3.5', provider: 'openai' }
+ ]
+ });
+ } catch (error) {
+ console.error('Error fetching models:', error);
+ return Response.json({ error: 'Failed to fetch models' }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/python/route.ts b/app/api/python/route.ts
new file mode 100644
index 0000000..95afdc8
--- /dev/null
+++ b/app/api/python/route.ts
@@ -0,0 +1,57 @@
+import { NextRequest, NextResponse } from "next/server";
+import { spawn } from 'child_process';
+import { writeFile } from 'fs/promises';
+import { v4 as uuidv4 } from 'uuid';
+import path from 'path';
+
+const TEMP_DIR = path.join(process.cwd(), 'tmp');
+
+export async function POST(req: NextRequest) {
+ try {
+ const { code } = await req.json();
+
+ // Generate a unique filename
+ const filename = path.join(TEMP_DIR, `${uuidv4()}.py`);
+
+ // Write the code to a temporary file
+ await writeFile(filename, code);
+
+ // Execute the Python code
+ const output = await new Promise((resolve, reject) => {
+ let stdout = '';
+ let stderr = '';
+
+ const process = spawn('python3', [filename], {
+ timeout: 10000, // 10 second timeout
+ });
+
+ process.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
+
+ process.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
+
+ process.on('close', (code) => {
+ if (code === 0) {
+ resolve(stdout);
+ } else {
+ reject(new Error(stderr || 'Execution failed'));
+ }
+ });
+
+ process.on('error', (err) => {
+ reject(err);
+ });
+ });
+
+ return NextResponse.json({ output });
+ } catch (error) {
+ console.error('Python execution error:', error);
+ return NextResponse.json(
+ { error: error instanceof Error ? error.message : 'Failed to execute Python code' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/tasks/route.ts b/app/api/tasks/route.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/api/tasks/route.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/api/websearch/route.ts b/app/api/websearch/route.ts
new file mode 100644
index 0000000..f6ad585
--- /dev/null
+++ b/app/api/websearch/route.ts
@@ -0,0 +1,25 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function POST(req: NextRequest) {
+ try {
+ const { query } = await req.json();
+
+ // Use a search API (e.g., Google Custom Search API, Bing Web Search API)
+ // For this example, we'll use DuckDuckGo's API
+ const response = await fetch(`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json`);
+ const data = await response.json();
+
+ return NextResponse.json({
+ results: data.RelatedTopics?.slice(0, 5).map((topic: any) => ({
+ title: topic.Text,
+ url: topic.FirstURL,
+ })) || [],
+ });
+ } catch (error) {
+ console.error('Web search error:', error);
+ return NextResponse.json(
+ { error: 'Failed to perform web search' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts
index 8d5b3f5..dc8d359 100644
--- a/app/auth/callback/route.ts
+++ b/app/auth/callback/route.ts
@@ -4,18 +4,23 @@ import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
export async function GET(request: Request) {
- const requestUrl = new URL(request.url);
- const code = requestUrl.searchParams.get("code");
+ try {
+ const requestUrl = new URL(request.url);
+ const code = requestUrl.searchParams.get("code");
- if (code) {
- const supabase = await createClient();
- const { error } = await supabase.auth.exchangeCodeForSession(code);
+ if (code) {
+ const supabase = await createClient();
+ const { error } = await supabase.auth.exchangeCodeForSession(code);
- if (!error) {
- return NextResponse.redirect(requestUrl.origin);
+ if (!error) {
+ return NextResponse.redirect(requestUrl.origin);
+ }
}
- }
- // Return the user to an error page with some instructions
- return NextResponse.redirect(`${requestUrl.origin}/auth-error`);
+ // Return the user to an error page with instructions
+ return NextResponse.redirect(`${requestUrl.origin}/auth-error`);
+ } catch (error) {
+ console.error("Error in auth callback:", error);
+ return NextResponse.redirect(`${requestUrl.origin}/auth-error`);
+ }
}
diff --git a/app/components/CryptoChart.tsx b/app/components/CryptoChart.tsx
new file mode 100644
index 0000000..09a85f8
--- /dev/null
+++ b/app/components/CryptoChart.tsx
@@ -0,0 +1,95 @@
+import { Line } from 'react-chartjs-2';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+ ChartOptions
+} from 'chart.js';
+import { useTheme } from 'next-themes';
+
+// Register Chart.js components
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend
+);
+
+interface CryptoChartProps {
+ data: {
+ timestamps: number[];
+ prices: number[];
+ };
+ coinName: string;
+ currency: string;
+}
+
+export function CryptoChart({ data, coinName, currency }: CryptoChartProps) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ const chartData = {
+ labels: data.timestamps.map(ts => new Date(ts).toLocaleDateString()),
+ datasets: [
+ {
+ label: `${coinName} Price`,
+ data: data.prices,
+ borderColor: isDark ? '#10b981' : '#059669',
+ backgroundColor: isDark ? 'rgba(16, 185, 129, 0.1)' : 'rgba(5, 150, 105, 0.1)',
+ fill: true,
+ tension: 0.4,
+ },
+ ],
+ };
+
+ const options: ChartOptions<'line'> = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'top' as const,
+ labels: {
+ color: isDark ? '#e5e7eb' : '#374151',
+ },
+ },
+ title: {
+ display: true,
+ text: `${coinName} Price Chart (${currency.toUpperCase()})`,
+ color: isDark ? '#e5e7eb' : '#374151',
+ },
+ },
+ scales: {
+ x: {
+ grid: {
+ color: isDark ? '#374151' : '#e5e7eb',
+ },
+ ticks: {
+ color: isDark ? '#e5e7eb' : '#374151',
+ },
+ },
+ y: {
+ grid: {
+ color: isDark ? '#374151' : '#e5e7eb',
+ },
+ ticks: {
+ color: isDark ? '#e5e7eb' : '#374151',
+ callback: (value) => `${currency.toUpperCase()} ${value.toLocaleString()}`,
+ },
+ },
+ },
+ };
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/CryptoPrice.tsx b/app/components/CryptoPrice.tsx
new file mode 100644
index 0000000..7916ff7
--- /dev/null
+++ b/app/components/CryptoPrice.tsx
@@ -0,0 +1,64 @@
+import { Card } from "@/components/ui/card";
+import { CryptoChart } from "./CryptoChart";
+import Image from "next/image";
+
+interface CryptoPriceProps {
+ data: {
+ symbol: string;
+ name: string;
+ price: number;
+ currency: string;
+ chartData: {
+ timestamps: number[];
+ prices: number[];
+ };
+ thumbnail: string;
+ marketCapRank: number;
+ };
+}
+
+export function CryptoPrice({ data }: CryptoPriceProps) {
+ const { symbol, name, price, currency, chartData, thumbnail, marketCapRank } = data;
+
+ return (
+
+
+
+
+
+
{name} ({symbol})
+
+
+ {currency.toUpperCase()} {price.toLocaleString(undefined, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })}
+
+ {marketCapRank && (
+
+ Rank #{marketCapRank}
+
+ )}
+
+
+
+
+
+ 7-day price history
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/custom/chat.tsx b/app/components/custom/chat.tsx
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/components/custom/chat.tsx
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/components/custom/task-list.tsx b/app/components/custom/task-list.tsx
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/components/custom/task-list.tsx
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/components/ui/badge.tsx b/app/components/ui/badge.tsx
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/components/ui/badge.tsx
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/components/ui/card.tsx b/app/components/ui/card.tsx
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/components/ui/card.tsx
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/components/ui/scroll-area.tsx b/app/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/components/ui/scroll-area.tsx
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/db/migrations/00001_create_tasks.sql b/app/db/migrations/00001_create_tasks.sql
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/db/migrations/00001_create_tasks.sql
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/db/mutations.ts b/app/db/mutations.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/db/mutations.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/hooks/useTaskManager.ts b/app/hooks/useTaskManager.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/hooks/useTaskManager.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index f487c0b..222d742 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,29 +1,17 @@
import { Metadata } from "next";
-
+import { Inter } from "next/font/google";
import { RootProvider } from "@/components/providers/root-provider";
-
import "./globals.css";
-import "../styles/dark-mode.css";
+
+const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- metadataBase: new URL("https://chainable.guru"),
- title: "Elron - AI web3 chatbot",
- description:
- "Elron is an AI chatbot that integrates with blockchain technologies.",
+ title: "Chainable",
+ description: "Secure blockchain integration with AI",
icons: {
- icon: [
- { url: "/favicon.ico", sizes: "any" },
- { url: "/icon.svg", type: "image/svg+xml", sizes: "any" },
- ],
- apple: [{ url: "/apple-icon.png", sizes: "180x180" }],
- shortcut: "/favicon.ico",
- },
- viewport: {
- width: "device-width",
- initialScale: 1,
- maximumScale: 1,
- userScalable: false,
- },
+ icon: "/favicon.ico",
+ apple: "/apple-touch-icon.png",
+ }
};
export default function RootLayout({
@@ -32,24 +20,8 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
-
-
-
-
-
-
-
-
+
+
{children}
diff --git a/app/lib/langchain/task-manager.ts b/app/lib/langchain/task-manager.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/lib/langchain/task-manager.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/lib/openai-config.ts b/app/lib/openai-config.ts
new file mode 100644
index 0000000..7e453cf
--- /dev/null
+++ b/app/lib/openai-config.ts
@@ -0,0 +1,13 @@
+import { Configuration } from 'openai-edge';
+
+export function getOpenAIConfig(customApiKey?: string) {
+ const apiKey = customApiKey || process.env.NEXT_PUBLIC_OPENAI_API_KEY;
+
+ if (!apiKey) {
+ throw new Error('OpenAI API key is required');
+ }
+
+ return new Configuration({
+ apiKey,
+ });
+}
\ No newline at end of file
diff --git a/app/lib/openai-stream.ts b/app/lib/openai-stream.ts
new file mode 100644
index 0000000..2145da4
--- /dev/null
+++ b/app/lib/openai-stream.ts
@@ -0,0 +1,68 @@
+import { createParser } from 'eventsource-parser';
+import { OpenAIStreamConfig } from './types';
+import { getOpenAIConfig } from './openai-config';
+
+export async function OpenAIStream(body: any, config: OpenAIStreamConfig) {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ // Use the OpenAI config from environment variables by default
+ const openaiConfig = getOpenAIConfig();
+
+ // Allow override of API key if provided in config
+ const apiKey = config.apiKey || openaiConfig.apiKey;
+
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${apiKey}`,
+ },
+ method: 'POST',
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const error = await res.json().catch(() => ({}));
+ throw new Error(JSON.stringify(error));
+ }
+
+ return new ReadableStream({
+ async start(controller) {
+ const parser = createParser((event) => {
+ if (event.type === 'event') {
+ try {
+ const data = JSON.parse(event.data);
+ const text = data.choices[0]?.delta?.content || '';
+
+ if (text) {
+ controller.enqueue(encoder.encode(text));
+ }
+ } catch (e) {
+ console.error('Parse error:', e);
+ }
+ }
+ });
+
+ // Stream the response
+ const reader = res.body?.getReader();
+ if (!reader) {
+ controller.close();
+ return;
+ }
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ controller.close();
+ break;
+ }
+ parser.feed(decoder.decode(value));
+ }
+ } catch (e) {
+ console.error('Stream error:', e);
+ controller.error(e);
+ }
+ },
+ });
+}
\ No newline at end of file
diff --git a/app/lib/queue.ts b/app/lib/queue.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/lib/queue.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/lib/services/coingecko.ts b/app/lib/services/coingecko.ts
new file mode 100644
index 0000000..45b4f16
--- /dev/null
+++ b/app/lib/services/coingecko.ts
@@ -0,0 +1,107 @@
+import { kv } from '@vercel/kv';
+
+const COINGECKO_API_URL = 'https://api.coingecko.com/api/v3';
+const RATE_LIMIT = {
+ WINDOW_MS: 60000, // 1 minute
+ MAX_REQUESTS: 30, // Maximum requests per minute (Free tier limit)
+};
+
+interface CoinGeckoPrice {
+ [key: string]: {
+ [currency: string]: number;
+ };
+}
+
+interface CoinGeckoMarketChart {
+ prices: [number, number][]; // [timestamp, price]
+ market_caps: [number, number][];
+ total_volumes: [number, number][];
+}
+
+export interface CoinGeckoSearchResult {
+ id: string;
+ symbol: string;
+ name: string;
+ market_cap_rank: number;
+ thumb: string;
+ large: string;
+}
+
+async function checkRateLimit(identifier: string): Promise {
+ const now = Date.now();
+ const windowStart = now - RATE_LIMIT.WINDOW_MS;
+
+ const usage = await kv.get<{ requests: number; timestamp: number }>(
+ `ratelimit:coingecko:${identifier}`
+ ) || { requests: 0, timestamp: now };
+
+ if (usage.timestamp < windowStart) {
+ usage.requests = 0;
+ usage.timestamp = now;
+ }
+
+ if (usage.requests >= RATE_LIMIT.MAX_REQUESTS) {
+ return false;
+ }
+
+ usage.requests++;
+ await kv.set(`ratelimit:coingecko:${identifier}`, usage, { ex: 60 });
+ return true;
+}
+
+export async function getCoinPrice(coinId: string, currency: string = 'usd'): Promise {
+ const canMakeRequest = await checkRateLimit('price');
+ if (!canMakeRequest) {
+ throw new Error('Rate limit exceeded for CoinGecko API');
+ }
+
+ const response = await fetch(
+ `${COINGECKO_API_URL}/simple/price?ids=${coinId}&vs_currencies=${currency}`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch coin price');
+ }
+
+ const data: CoinGeckoPrice = await response.json();
+ return data[coinId][currency];
+}
+
+export async function getCoinMarketChart(
+ coinId: string,
+ currency: string = 'usd',
+ days: number = 7
+): Promise {
+ const canMakeRequest = await checkRateLimit('chart');
+ if (!canMakeRequest) {
+ throw new Error('Rate limit exceeded for CoinGecko API');
+ }
+
+ const response = await fetch(
+ `${COINGECKO_API_URL}/coins/${coinId}/market_chart?vs_currency=${currency}&days=${days}`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch market chart');
+ }
+
+ return response.json();
+}
+
+export async function searchCoins(query: string): Promise {
+ const canMakeRequest = await checkRateLimit('search');
+ if (!canMakeRequest) {
+ throw new Error('Rate limit exceeded for CoinGecko API');
+ }
+
+ const response = await fetch(
+ `${COINGECKO_API_URL}/search?query=${encodeURIComponent(query)}`
+ );
+
+ if (!response.ok) {
+ throw new Error('Failed to search coins');
+ }
+
+ const data = await response.json();
+ return data.coins;
+}
\ No newline at end of file
diff --git a/app/lib/supabase/types.ts b/app/lib/supabase/types.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/lib/supabase/types.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/lib/taskProcessor.ts b/app/lib/taskProcessor.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/lib/taskProcessor.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/lib/types.ts b/app/lib/types.ts
new file mode 100644
index 0000000..c7ced27
--- /dev/null
+++ b/app/lib/types.ts
@@ -0,0 +1,4 @@
+export interface OpenAIStreamConfig {
+ apiKey: string;
+ // Add other config options as needed
+}
\ No newline at end of file
diff --git a/app/lib/types/functions.ts b/app/lib/types/functions.ts
new file mode 100644
index 0000000..7f9b3f5
--- /dev/null
+++ b/app/lib/types/functions.ts
@@ -0,0 +1,15 @@
+export interface WeatherParams {
+ location: string;
+ unit?: 'celsius' | 'fahrenheit';
+}
+
+export interface CryptoPriceParams {
+ symbol: string;
+ currency?: 'USD' | 'EUR' | 'GBP';
+}
+
+export interface FunctionResponse {
+ success: boolean;
+ data?: T;
+ error?: string;
+}
\ No newline at end of file
diff --git a/app/providers.tsx b/app/providers.tsx
new file mode 100644
index 0000000..359cb3c
--- /dev/null
+++ b/app/providers.tsx
@@ -0,0 +1 @@
+// This file will be removed as we're using the original RootProvider
\ No newline at end of file
diff --git a/bun.lockb b/bun.lockb
index d7d72f6..ebbaa06 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components/chat.tsx b/components/chat.tsx
new file mode 100644
index 0000000..eab9961
--- /dev/null
+++ b/components/chat.tsx
@@ -0,0 +1,15 @@
+import { useChat } from "@/lib/hooks/use-chat";
+
+export function Chat() {
+ const { messages, input, handleInputChange, handleSubmit } = useChat({
+ api: {
+ request: async (messages) => {
+ const { sendMessage } = useChat();
+ const response = await sendMessage(messages[messages.length - 1].content);
+ return response;
+ },
+ },
+ });
+
+ // ... (keep the rest of the component code)
+}
\ No newline at end of file
diff --git a/components/custom/chat-header.tsx b/components/custom/chat-header.tsx
index 9b514b9..d913da3 100644
--- a/components/custom/chat-header.tsx
+++ b/components/custom/chat-header.tsx
@@ -1,54 +1,75 @@
"use client";
-import { ConnectButton } from "@rainbow-me/rainbowkit";
-import Link from "next/link";
import { useRouter } from "next/navigation";
import { useWindowSize } from "usehooks-ts";
import { ModelSelector } from "@/components/custom/model-selector";
import { SidebarToggle } from "@/components/custom/sidebar-toggle";
+import { SettingsDialog } from "@/components/custom/settings-dialog";
import { Button } from "@/components/ui/button";
import { BetterTooltip } from "@/components/ui/tooltip";
+import { toast } from "sonner";
-import { PlusIcon } from "./icons";
+import { PlusIcon, HistoryIcon, SettingsIcon } from "./icons";
import { useSidebar } from "../ui/sidebar";
export function ChatHeader({ selectedModelId }: { selectedModelId: string }) {
const router = useRouter();
- const { open } = useSidebar();
+ const { open, toggle } = useSidebar();
const { width: windowWidth } = useWindowSize();
+ const isMobile = windowWidth < 768;
+
+ const handleNewChat = () => {
+ router.push("/");
+ router.refresh();
+ toast.success("Started new chat");
+ };
+
+ const handleHistory = () => {
+ if (isMobile) {
+ toggle();
+ }
+ router.push("/history");
+ };
return (
-
+
- {(!open || windowWidth < 768) && (
+
+ {(!open || isMobile) && (
{
- router.push("/");
- router.refresh();
- }}
+ size="icon"
+ className="h-9 w-9"
+ onClick={handleNewChat}
>
-
- New Chat
+
+ New Chat
)}
+
-
-
+
+
+ {!open && (
+
+
+
+ History
+
+
+ )}
+
);
diff --git a/components/custom/chat.tsx b/components/custom/chat.tsx
index 07ccaa5..f8db1ea 100644
--- a/components/custom/chat.tsx
+++ b/components/custom/chat.tsx
@@ -4,7 +4,7 @@ import { useChat } from "ai/react";
import type { Message, Attachment } from "ai";
import { AnimatePresence } from "framer-motion";
import { KeyboardIcon } from "lucide-react";
-import { useState, useEffect, type ClipboardEvent } from "react";
+import { useState, useEffect } from "react";
import useSWR, { useSWRConfig } from "swr";
import { useWindowSize } from "usehooks-ts";
import { Progress } from "@/components/ui/progress";
@@ -40,8 +40,8 @@ export function Chat({
selectedModelId: string;
}) {
const { mutate } = useSWRConfig();
- const { width: windowWidth = 1920, height: windowHeight = 1080 } =
- useWindowSize();
+ const [streamingResponse, setStreamingResponse] = useState
(null);
+ const { width: windowWidth = 1920, height: windowHeight = 1080 } = useWindowSize();
const {
messages,
@@ -89,184 +89,47 @@ export function Chat({
error: null,
});
- const handleFileUpload = async (file: File) => {
- if (!file) return;
-
- if (file.size > 10 * 1024 * 1024) {
- toast.error("File size must be less than 10MB");
- return;
- }
-
- setFileUpload({ progress: 0, uploading: true, error: null });
-
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- const formData = new FormData();
- formData.append("file", file);
-
- xhr.upload.addEventListener("progress", (e) => {
- if (e.lengthComputable) {
- const progress = Math.round((e.loaded * 100) / e.total);
- setFileUpload((prev) => ({ ...prev, progress }));
+ // Set up streaming response handler
+ useEffect(() => {
+ const eventSource = new EventSource(`/api/chat/stream?chatId=${id}`);
+
+ eventSource.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data.type === 'intermediate') {
+ setStreamingResponse(data);
+ } else if (data.type === 'final') {
+ setStreamingResponse(null);
}
- });
+ } catch (error) {
+ console.error('Error parsing streaming response:', error);
+ }
+ };
- xhr.addEventListener("load", () => {
- if (xhr.status === 200) {
- const response = JSON.parse(xhr.responseText);
- toast.success("File uploaded successfully");
- append({
- role: "user",
- content: `[File uploaded: ${file.name}](${response.url})`,
- });
- resolve(response);
- } else {
- setFileUpload((prev) => ({
- ...prev,
- error: "Upload failed",
- }));
- toast.error("Failed to upload file");
- reject(new Error("Upload failed"));
- }
- setFileUpload((prev) => ({ ...prev, uploading: false }));
- });
-
- xhr.addEventListener("error", () => {
- setFileUpload((prev) => ({
- ...prev,
- error: "Upload failed",
- uploading: false,
- }));
- toast.error("Failed to upload file");
- reject(new Error("Upload failed"));
- });
-
- xhr.open("POST", "/api/upload");
- xhr.send(formData);
- });
- };
+ return () => {
+ eventSource.close();
+ };
+ }, [id]);
return (
- <>
-
-
-
- {messages.length === 0 &&
}
-
- {messages.map((message, index) => (
-
vote.message_id === message.id)}
- />
- ))}
-
- {isLoading &&
- messages.length > 0 &&
- messages[messages.length - 1].role === "user" && (
-
- )}
-
-
-
-
-
+
+
+
+
-
-
- {block && block.isVisible && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
⌘ / to focus input
-
⌘ K to clear chat
-
ESC to stop generation
-
-
-
+
+
-
- {fileUpload.uploading && (
-
-
-
- Uploading... {fileUpload.progress}%
-
-
- )}
-
-
{
- const file = e.target.files?.[0];
- if (file) handleFileUpload(file);
- }}
- className="hidden"
- id="file-upload"
- accept="image/*,.pdf,.doc,.docx,.txt"
- />
- >
+
);
}
diff --git a/components/custom/database-query.tsx b/components/custom/database-query.tsx
new file mode 100644
index 0000000..72260b1
--- /dev/null
+++ b/components/custom/database-query.tsx
@@ -0,0 +1,81 @@
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { DatabaseIcon } from './icons';
+
+interface DatabaseQueryToolProps {
+ type: 'select' | 'insert' | 'update' | 'delete';
+ args: {
+ table: string;
+ query?: any;
+ data?: any;
+ };
+ result?: {
+ success: boolean;
+ data?: any;
+ error?: string;
+ };
+}
+
+export function DatabaseQueryTool({ type, args, result }: DatabaseQueryToolProps) {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const getActionText = () => {
+ switch (type) {
+ case 'select':
+ return 'Querying';
+ case 'insert':
+ return 'Inserting into';
+ case 'update':
+ return 'Updating';
+ case 'delete':
+ return 'Deleting from';
+ default:
+ return 'Processing';
+ }
+ };
+
+ if (!result) {
+ return (
+
+
+
+
+
+
+ {getActionText()} {args.table}
+
+
+
⋯
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {result.success ? (
+ <>
+
+ {type === 'select' && 'Query Results'}
+ {type === 'insert' && 'Insert Results'}
+ {type === 'update' && 'Update Results'}
+ {type === 'delete' && 'Delete Results'}
+
+
+ {JSON.stringify(result.data, null, 2)}
+
+ >
+ ) : (
+
{result.error}
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/file-system.tsx b/components/custom/file-system.tsx
new file mode 100644
index 0000000..1e4e0d2
--- /dev/null
+++ b/components/custom/file-system.tsx
@@ -0,0 +1,85 @@
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { FileIcon } from './icons';
+import { Button } from '../ui/button';
+
+interface FileSystemToolProps {
+ type: 'read' | 'write' | 'list';
+ args: {
+ path?: string;
+ content?: string;
+ };
+ result?: {
+ success: boolean;
+ data?: string | string[];
+ error?: string;
+ };
+}
+
+export function FileSystemTool({ type, args, result }: FileSystemToolProps) {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const getActionText = () => {
+ switch (type) {
+ case 'read':
+ return 'Reading file';
+ case 'write':
+ return 'Writing to file';
+ case 'list':
+ return 'Listing directory';
+ default:
+ return 'Processing';
+ }
+ };
+
+ if (!result) {
+ return (
+
+
+
+
+
+
+ {getActionText()} {args.path}
+
+
+
⋯
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {result.success ? (
+ <>
+ {type === 'read' && (
+
+ {result.data as string}
+
+ )}
+ {type === 'list' && (
+
+ {(result.data as string[]).map((item, index) => (
+ {item}
+ ))}
+
+ )}
+ {type === 'write' && (
+
Successfully wrote to {args.path}
+ )}
+ >
+ ) : (
+
{result.error}
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/history-item.tsx b/components/custom/history-item.tsx
new file mode 100644
index 0000000..37b7cc2
--- /dev/null
+++ b/components/custom/history-item.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { MessageSquare, Trash } from "lucide-react";
+import { formatDistanceToNow } from "date-fns";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { toast } from "sonner";
+
+interface HistoryItemProps {
+ id: string;
+ title: string;
+ createdAt: string;
+ onDelete: () => void;
+}
+
+export function HistoryItem({ id, title, createdAt, onDelete }: HistoryItemProps) {
+ const pathname = usePathname();
+ const isActive = pathname === `/chat/${id}`;
+
+ const handleDelete = async (e: React.MouseEvent) => {
+ e.preventDefault();
+ try {
+ const response = await fetch(`/api/history/${id}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) throw new Error('Failed to delete chat');
+
+ onDelete();
+ toast.success('Chat deleted');
+ } catch (error) {
+ console.error('Error deleting chat:', error);
+ toast.error('Failed to delete chat');
+ }
+ };
+
+ return (
+
+
+
+
{title}
+
+ {formatDistanceToNow(new Date(createdAt), { addSuffix: true })}
+
+
+
+
+ Delete
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/history.tsx b/components/custom/history.tsx
new file mode 100644
index 0000000..9e82af8
--- /dev/null
+++ b/components/custom/history.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useCallback } from "react";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { cn } from "@/lib/utils";
+import { HistoryItem } from "./history-item";
+import useSWR from "swr";
+import { fetcher } from "@/lib/utils";
+
+export function History() {
+ const router = useRouter();
+ const { data: history, error, mutate } = useSWR('/api/history', fetcher);
+
+ const handleClearHistory = useCallback(async () => {
+ try {
+ const response = await fetch('/api/history', {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) throw new Error('Failed to clear history');
+
+ await mutate();
+ toast.success('History cleared');
+ router.push('/');
+ } catch (error) {
+ console.error('Error clearing history:', error);
+ toast.error('Failed to clear history');
+ }
+ }, [mutate, router]);
+
+ if (error) {
+ return (
+
+
Failed to load history
+
+ );
+ }
+
+ if (!history) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
History
+
+ Clear
+
+
+
+
+ {history.length === 0 ? (
+
+ No history yet
+
+ ) : (
+ history.map((item: any) => (
+
+ ))
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/icons.tsx b/components/custom/icons.tsx
index fc2472a..936d1cd 100644
--- a/components/custom/icons.tsx
+++ b/components/custom/icons.tsx
@@ -1,5 +1,6 @@
import { cn } from "@/lib/utils";
import { LucideProps } from "lucide-react";
+import { Plus, History, Settings, MessageSquare, Wallet, Search, Code, Database } from 'lucide-react';
export const BotIcon = () => {
return (
@@ -413,11 +414,11 @@ export const PencilEditIcon = ({ size = 16 }: { size?: number }) => {
-
- );
+ d="M11.75 0.189331L12.2803 0.719661L15.2803 3.71966L15.8107 4.24999L15.2803 4.78032L13.7374 9.32322C13.1911 9.8696 12.3733 9.97916 11.718 9.65188L9.54863 13.5568C8.71088 15.0648 7.12143 16 5.39639 16H0.75H0V15.25V10.6036C0 8.87856 0.935237 7.28911 2.4432 6.45136L6.34811 4.28196C6.02084 3.62674 6.13039 2.80894 6.67678 2.26255L8.21967 0.719661L8.75 0.189331ZM7.3697 5.43035L10.5696 8.63029L8.2374 12.8283C7.6642 13.8601 6.57668 14.5 5.39639 14.5H2.56066L5.53033 11.5303L4.46967 10.4697L1.5 13.4393V10.6036C1.5 9.42331 2.1399 8.33579 3.17166 7.76259L7.3697 5.43035ZM12.6768 8.26256C12.5791 8.36019 12.4209 8.36019 12.3232 8.26255L12.0303 7.96966L8.03033 3.96966L7.73744 3.67677C7.63981 3.57914 7.63981 3.42085 7.73744 3.32321L8.75 2.31065L13.6893 7.24999L12.6768 8.26256Z"
+ fill="currentColor"
+ >
+
+);
};
export const CheckedSquare = ({ size = 16 }: { size?: number }) => {
@@ -511,7 +512,7 @@ export const InfoIcon = ({ size = 16 }: { size?: number }) => {
@@ -594,24 +595,7 @@ export const MoreHorizontalIcon = ({ size = 16 }: { size?: number }) => {
);
};
-export const MessageIcon = ({ size = 16 }: { size?: number }) => {
- return (
-
-
-
- );
-};
+export const MessageIcon = MessageSquare;
export const CrossIcon = ({ size = 16 }: { size?: number }) => (
(
);
-export const PlusIcon = ({ size = 16 }: { size?: number }) => (
-
-
-
-);
-
-export const CopyIcon = ({ size = 16 }: { size?: number }) => (
-
-
-
-);
-
-export const ThumbUpIcon = ({ size = 16 }: { size?: number }) => (
-
-
-
-);
-
-export const ThumbDownIcon = ({ size = 16 }: { size?: number }) => (
-
-
-
-);
-
-export const ChevronDownIcon = ({ size = 16 }: { size?: number }) => (
-
-
-
-);
-
-export const SparklesIcon = ({ size = 16 }: { size?: number }) => (
-
-
-
-
-
-);
-
-export const CheckCirclFillIcon = ({ size = 16 }: { size?: number }) => {
- return (
-
-
-
- );
-};
-
-export const SupabaseIcon = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
+export const PlusIcon = Plus;
+export const HistoryIcon = History;
+export const SettingsIcon = Settings;
+export const WalletIcon = Wallet;
+export const SearchIcon = Search;
+export const CodeIcon = Code;
+export const DatabaseIcon = Database;
diff --git a/components/custom/message.tsx b/components/custom/message.tsx
index 67bde09..3508cc7 100644
--- a/components/custom/message.tsx
+++ b/components/custom/message.tsx
@@ -3,11 +3,19 @@
import { Message } from "ai";
import cx from "classnames";
import { motion } from "framer-motion";
-import { FileIcon } from "lucide-react";
+import { FileIcon, MoreVertical, WalletIcon } from "lucide-react";
import Image from "next/image";
-import { Dispatch, SetStateAction } from "react";
+import { Dispatch, SetStateAction, useState } from "react";
import { Vote } from "@/lib/supabase/types";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useWalletState } from "@/hooks/useWalletState";
import { UIBlock } from "./block";
import { DocumentToolCall, DocumentToolResult } from "./document";
@@ -17,6 +25,36 @@ import { MessageActions } from "./message-actions";
import { PreviewAttachment } from "./preview-attachment";
import { Weather } from "./weather";
+interface StreamingResponse {
+ type: 'intermediate' | 'final';
+ content: string;
+ data?: any;
+}
+
+const ImageWithFallback = ({ src, alt, ...props }: { src: string; alt: string; width: number; height: number; className?: string }) => {
+ const [error, setError] = useState(false);
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ setError(true)}
+ className={cx("object-cover hover:scale-105 transition-transform duration-300", props.className)}
+ />
+
+ );
+};
+
export const PreviewMessage = ({
chatId,
message,
@@ -24,6 +62,7 @@ export const PreviewMessage = ({
setBlock,
vote,
isLoading,
+ streamingResponse,
}: {
chatId: string;
message: Message;
@@ -31,31 +70,74 @@ export const PreviewMessage = ({
setBlock: Dispatch
>;
vote: Vote | undefined;
isLoading: boolean;
+ streamingResponse?: StreamingResponse;
}) => {
+ const [showActions, setShowActions] = useState(false);
+ const { isConnected, networkInfo, isCorrectNetwork } = useWalletState();
+
const renderContent = () => {
try {
+ if (streamingResponse && isLoading) {
+ return (
+
+
+ {streamingResponse.content && (
+
+ {streamingResponse.content}
+
+ )}
+
+ );
+ }
+
const content = JSON.parse(message.content);
+ const isWalletMessage = content.text?.toLowerCase().includes('wallet') ||
+ content.text?.toLowerCase().includes('balance');
+
return (
{content.text && (
-
{content.text}
+
+
{content.text}
+ {isWalletMessage && (
+
+
+ {isConnected ? (
+
+ Connected to {networkInfo?.name || "Unknown Network"}
+ {!isCorrectNetwork && " (Unsupported Network)"}
+
+ ) : (
+
+ Wallet not connected
+
+ )}
+
+ )}
+
)}
{content.attachments && content.attachments.length > 0 && (
-
+
{content.attachments.map((att: any, index: number) => (
-
+
{att.type.startsWith("image/") ? (
-
) : (
-
-
-
{att.name}
+
+
+ {att.name}
)}
@@ -65,25 +147,30 @@ export const PreviewMessage = ({
);
} catch {
- return
{message.content}
;
+ return
{message.content}
;
}
};
return (
setShowActions(true)}
+ onMouseLeave={() => setShowActions(false)}
>
{message.role === "assistant" && (
-
-
+
+
)}
@@ -101,7 +188,7 @@ export const PreviewMessage = ({
const { result } = toolInvocation;
return (
-
+
{toolName === "getWeather" ? (
) : toolName === "createDocument" ? (
@@ -126,7 +213,9 @@ export const PreviewMessage = ({
setBlock={setBlock}
/>
) : (
-
{JSON.stringify(result, null, 2)}
+
+ {JSON.stringify(result, null, 2)}
+
)}
);
@@ -134,7 +223,7 @@ export const PreviewMessage = ({
return (
@@ -165,6 +254,30 @@ export const PreviewMessage = ({
isLoading={isLoading}
/>
+
+ {message.role === "user" && showActions && (
+
+
+
+
+
+
+
+
+ navigator.clipboard.writeText(message.content)}>
+ Copy message
+
+
+ Delete message
+
+
+
+
+ )}
);
diff --git a/components/custom/model-selector.tsx b/components/custom/model-selector.tsx
index 19cefad..02b9565 100644
--- a/components/custom/model-selector.tsx
+++ b/components/custom/model-selector.tsx
@@ -1,78 +1,66 @@
"use client";
-import { startTransition, useMemo, useOptimistic, useState } from "react";
+import { useEffect } from 'react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { useModel } from "@/lib/hooks/use-model";
-import { models } from "@/ai/models";
-import { saveModelId } from "@/app/(chat)/actions";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { cn } from "@/lib/utils";
-
-import { CheckCirclFillIcon, ChevronDownIcon } from "./icons";
+function ModelLabel({ model }: { model: { id: string; name: string; provider: string } }) {
+ return (
+
+ {model.name}
+ {model.provider === 'ollama' && (
+
+ Local
+
+ )}
+
+ );
+}
export function ModelSelector({
selectedModelId,
className,
}: {
selectedModelId: string;
-} & React.ComponentProps
) {
- const [open, setOpen] = useState(false);
- const [optimisticModelId, setOptimisticModelId] =
- useOptimistic(selectedModelId);
+ className?: string;
+}) {
+ const {
+ models,
+ selectedModel,
+ isLoading,
+ handleModelChange,
+ fetchModels
+ } = useModel();
- const selectModel = useMemo(
- () => models.find((model) => model.id === optimisticModelId),
- [optimisticModelId],
- );
+ useEffect(() => {
+ fetchModels();
+ }, [fetchModels]);
return (
-
-
-
- {selectModel?.label}
-
-
-
-
+
+
+
+ {isLoading ? (
+ "Loading models..."
+ ) : (
+ selectedModel ? (
+
+ ) : (
+ "Select a model"
+ )
+ )}
+
+
+
{models.map((model) => (
- {
- setOpen(false);
-
- startTransition(() => {
- setOptimisticModelId(model.id);
- saveModelId(model.id);
- });
- }}
- className="gap-4 group/item flex flex-row justify-between items-center"
- data-active={model.id === optimisticModelId}
- >
-
- {model.label}
- {model.description && (
-
- {model.description}
-
- )}
-
-
-
-
-
+
+
+
))}
-
-
+
+
);
}
diff --git a/components/custom/multimodal-input.tsx b/components/custom/multimodal-input.tsx
index 5966154..7b7cf32 100644
--- a/components/custom/multimodal-input.tsx
+++ b/components/custom/multimodal-input.tsx
@@ -1,37 +1,14 @@
"use client";
-import cx from "classnames";
-import { motion } from "framer-motion";
-import { X } from "lucide-react";
-import React, {
- useRef,
- useEffect,
- useState,
- useCallback,
- Dispatch,
- SetStateAction,
- ChangeEvent,
-} from "react";
+import { useRef, useState, useCallback } from "react";
import { toast } from "sonner";
-import { useLocalStorage, useWindowSize } from "usehooks-ts";
-
-import { useWalletState } from "@/hooks/useWalletState";
-import { createClient } from "@/lib/supabase/client";
-import { sanitizeUIMessages } from "@/lib/utils";
-
-import { ArrowUpIcon, PaperclipIcon, StopIcon } from "./icons";
+import { Button } from "@/components/ui/button";
+import { WalletButton } from "@/components/custom/wallet-button";
+import { BetterTooltip } from "@/components/ui/tooltip";
+import { PaperclipIcon, StopIcon, ArrowUpIcon } from "./icons";
import { PreviewAttachment } from "./preview-attachment";
-import { Button } from "../ui/button";
-import { Textarea } from "../ui/textarea";
-import { ChatSkeleton } from "./chat-skeleton";
-
-import type { Attachment as SupabaseAttachment } from "@/types/supabase";
-import type {
- Attachment,
- ChatRequestOptions,
- CreateMessage,
- Message,
-} from "ai";
+import { motion } from "framer-motion";
+import cx from "classnames";
const suggestedActions = [
{
@@ -42,8 +19,7 @@ const suggestedActions = [
{
title: "Update an existing document",
label: 'with the description "Add more details"',
- action:
- 'Update the document with ID "123" with the description "Add more details"',
+ action: 'Update the document with ID "123" with the description "Add more details"',
},
{
title: "Request suggestions for a document",
@@ -66,41 +42,11 @@ const suggestedActions = [
action: "Check the state of my connected wallet",
},
];
-// Add type for temp attachments
-type TempAttachment = {
- url: string;
- name: string;
- contentType: string;
- path?: string;
-};
-// Add type for staged files
-interface StagedFile {
- id: string;
- file: File;
- previewUrl: string;
- status: "staging" | "uploading" | "complete" | "error";
-}
-
-interface MultimodalInputProps {
- input: string;
- setInput: (value: string) => void;
- isLoading: boolean;
- stop: () => void;
- attachments: Attachment[];
- setAttachments: Dispatch>;
- messages: Message[];
- setMessages: Dispatch>;
- append: (
- message: Message | CreateMessage,
- chatRequestOptions?: ChatRequestOptions,
- ) => Promise;
- handleSubmit: (
- event?: { preventDefault?: () => void },
- chatRequestOptions?: ChatRequestOptions,
- ) => void;
- className?: string;
- chatId: string;
+interface FileUploadState {
+ progress: number;
+ uploading: boolean;
+ error: string | null;
}
export function MultimodalInput({
@@ -108,536 +54,231 @@ export function MultimodalInput({
setInput,
isLoading,
stop,
- attachments,
- setAttachments,
messages,
setMessages,
append,
handleSubmit,
- className,
chatId,
-}: MultimodalInputProps) {
- const textareaRef = useRef(null);
- const { width } = useWindowSize();
- const supabase = createClient();
- const { address, isConnected, chainId, networkInfo, isCorrectNetwork } =
- useWalletState();
-
- const [uploadProgress, setUploadProgress] = useState(0);
- const [stagedFiles, setStagedFiles] = useState([]);
- const [expectingText, setExpectingText] = useState(false);
- const stagedFileNames = useRef>(new Set());
-
- useEffect(() => {
- if (textareaRef.current) {
- adjustHeight();
- }
- }, []);
-
- const adjustHeight = () => {
- if (textareaRef.current) {
- textareaRef.current.style.height = "auto";
- textareaRef.current.style.height = `${textareaRef.current.scrollHeight + 2}px`;
- }
- };
-
- const [localStorageInput, setLocalStorageInput] = useLocalStorage(
- "input",
- "",
- );
-
- useEffect(() => {
- if (textareaRef.current) {
- const domValue = textareaRef.current.value;
- // Prefer DOM value over localStorage to handle hydration
- const finalValue = domValue || localStorageInput || "";
- setInput(finalValue);
- adjustHeight();
- }
- // Only run once after hydration
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- setLocalStorageInput(input);
- }, [input, setLocalStorageInput]);
-
- const handleInput = (event: React.ChangeEvent) => {
- setInput(event.target.value);
- adjustHeight();
- };
-
+}: {
+ input: string;
+ setInput: (value: string) => void;
+ isLoading: boolean;
+ stop: () => void;
+ messages: any[];
+ setMessages: (messages: any[]) => void;
+ append: (message: any) => void;
+ handleSubmit: (e: React.FormEvent) => void;
+ chatId: string;
+}) {
const fileInputRef = useRef(null);
-
- // Create blob URLs for file previews
- const createStagedFile = useCallback((file: File): StagedFile => {
- return {
- id: crypto.randomUUID(),
- file,
- previewUrl: URL.createObjectURL(file),
- status: "staging",
- };
- }, []);
-
- // Clean up blob URLs when files are removed
- const removeStagedFile = useCallback((fileId: string) => {
- setStagedFiles((prev) => {
- const file = prev.find((f) => f.id === fileId);
- if (file) {
- URL.revokeObjectURL(file.previewUrl);
- }
- const updatedFiles = prev.filter((f) => f.id !== fileId);
- if (file) {
- stagedFileNames.current.delete(file.file.name);
- }
- return updatedFiles;
- });
- }, []);
-
- // Clean up all blob URLs on unmount
- useEffect(() => {
- return () => {
- stagedFiles.forEach((file) => {
- URL.revokeObjectURL(file.previewUrl);
- });
- };
- }, [stagedFiles]);
-
- const submitForm = useCallback(async () => {
- if (!input && attachments.length === 0) return;
-
- const isWalletQuery =
- input.toLowerCase().includes("wallet") ||
- input.toLowerCase().includes("balance");
-
- // Set expecting text based on input type
- setExpectingText(true);
-
- if (isWalletQuery) {
- if (!isConnected) {
- toast.error("Please connect your wallet first");
- return;
- }
- if (!isCorrectNetwork) {
- toast.error("Please switch to Base Mainnet or Base Sepolia");
- return;
- }
+ const [fileUpload, setFileUpload] = useState({
+ progress: 0,
+ uploading: false,
+ error: null,
+ });
+ const [attachments, setAttachments] = useState([]);
+
+ const handleFileUpload = async (file: File) => {
+ if (!file) return;
+
+ if (file.size > 10 * 1024 * 1024) {
+ toast.error("File size must be less than 10MB");
+ return;
}
- const messageContent = isWalletQuery
- ? {
- text: input,
- attachments: attachments.map((att) => ({
- url: att.url,
- name: att.name,
- type: att.contentType,
- })),
- walletAddress: address,
- chainId,
- network: networkInfo?.name,
- isWalletConnected: isConnected,
- isCorrectNetwork,
- }
- : {
- text: input,
- attachments: attachments.map((att) => ({
- url: att.url,
- name: att.name,
- type: att.contentType,
- })),
- };
+ setFileUpload({ progress: 0, uploading: true, error: null });
try {
- await append(
- {
- role: "user",
- content: JSON.stringify(messageContent),
- },
- {
- experimental_attachments: attachments,
- },
- );
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('chatId', chatId);
- setInput("");
- setAttachments([]);
- setLocalStorageInput("");
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) throw new Error('Upload failed');
+
+ const data = await response.json();
+ toast.success("File uploaded successfully");
+
+ setAttachments(prev => [...prev, {
+ url: data.url,
+ name: file.name,
+ type: file.type
+ }]);
+
+ append({
+ role: "user",
+ content: `[File uploaded: ${file.name}](${data.url})`,
+ });
} catch (error) {
- console.error("Error sending message:", error);
- toast.error("Failed to send message");
+ console.error('Error uploading file:', error);
+ toast.error(`Failed to upload ${file.name}`);
+ setFileUpload(prev => ({
+ ...prev,
+ error: "Upload failed",
+ }));
} finally {
- // Reset expectingText when response is received
- setExpectingText(false);
+ setFileUpload(prev => ({ ...prev, uploading: false }));
}
- }, [
- input,
- attachments,
- append,
- setInput,
- setLocalStorageInput,
- address,
- chainId,
- setAttachments,
- isConnected,
- isCorrectNetwork,
- networkInfo,
- ]);
-
- const handleSuggestedAction = useCallback(
- (action: string) => {
- const isWalletAction =
- action.toLowerCase().includes("wallet") ||
- action.toLowerCase().includes("balance");
-
- if (isWalletAction) {
- if (!isConnected) {
- toast.error("Please connect your wallet first");
- return;
- }
- if (!isCorrectNetwork) {
- toast.error("Please switch to Base Mainnet or Base Sepolia");
- return;
- }
- }
-
- setInput(action);
- submitForm();
- },
- [isConnected, isCorrectNetwork, setInput, submitForm],
- );
-
- const handleFileChange = useCallback(
- async (event: ChangeEvent) => {
- const files = Array.from(event.target.files || []);
-
- // Create staged files with blob URLs
- const newStagedFiles = files
- .filter((file) => !stagedFileNames.current.has(file.name))
- .map((file) => {
- stagedFileNames.current.add(file.name);
- return createStagedFile(file);
- });
- setStagedFiles((prev) => [...prev, ...newStagedFiles]);
-
- try {
- // Upload each file
- for (const stagedFile of newStagedFiles) {
- setStagedFiles((prev) =>
- prev.map((f) =>
- f.id === stagedFile.id ? { ...f, status: "uploading" } : f,
- ),
- );
-
- const formData = new FormData();
- formData.append("file", stagedFile.file);
- formData.append("chatId", chatId);
-
- const response = await fetch("/api/files/upload", {
- method: "POST",
- body: formData,
- });
-
- if (!response.ok) throw new Error("Upload failed");
-
- const data = await response.json();
-
- // Add to attachments on successful upload
- setAttachments((current) => [
- ...current,
- {
- url: data.url,
- name: stagedFile.file.name,
- contentType: stagedFile.file.type,
- path: data.path,
- },
- ]);
+ };
- // Mark as complete and remove from staged files
- setStagedFiles((prev) =>
- prev.map((f) =>
- f.id === stagedFile.id ? { ...f, status: "complete" } : f,
- ),
- );
- removeStagedFile(stagedFile.id);
- }
+ const handleFileChange = useCallback(async (event: React.ChangeEvent) => {
+ const files = Array.from(event.target.files || []);
+ for (const file of files) {
+ await handleFileUpload(file);
+ }
+ }, []);
- toast.success("Files uploaded successfully");
- } catch (error) {
- console.error("Error uploading files:", error);
- toast.error("Failed to upload one or more files");
+ const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
+ const items = Array.from(e.clipboardData.items);
+ const imageItems = items.filter(item => item.type.startsWith('image/'));
- // Mark failed files
- newStagedFiles.forEach((file) => {
- setStagedFiles((prev) =>
- prev.map((f) => (f.id === file.id ? { ...f, status: "error" } : f)),
- );
- });
- } finally {
- if (fileInputRef.current) {
- fileInputRef.current.value = "";
+ if (imageItems.length > 0) {
+ e.preventDefault();
+ for (const item of imageItems) {
+ const file = item.getAsFile();
+ if (file) {
+ await handleFileUpload(file);
}
}
- },
- [chatId, createStagedFile, removeStagedFile, setAttachments],
- );
-
- // Focus management
- useEffect(() => {
- if (textareaRef.current) {
- textareaRef.current.focus();
}
- }, [messages.length]); // Refocus after new message
-
- // Auto-focus on mount
- useEffect(() => {
- const timer = setTimeout(() => {
- textareaRef.current?.focus();
- }, 100);
- return () => clearTimeout(timer);
}, []);
- const handlePaste = useCallback(
- async (e: React.ClipboardEvent) => {
- console.log("🔍 Paste event detected");
-
- const clipboardData = e.clipboardData;
- if (!clipboardData) return;
-
- // Check for images in clipboard
- const items = Array.from(clipboardData.items);
- const imageItems = items.filter(
- (item) => item.kind === "file" && item.type.startsWith("image/"),
- );
-
- if (imageItems.length > 0) {
- e.preventDefault();
- console.log("📸 Found image in clipboard");
-
- // Convert clipboard items to files
- const files = imageItems
- .map((item) => item.getAsFile())
- .filter((file): file is File => file !== null)
- .map(
- (file) =>
- new File(
- [file],
- `screenshot-${Date.now()}.${file.type.split("/")[1] || "png"}`,
- { type: file.type },
- ),
- );
-
- // Create staged files with blob URLs
- const newStagedFiles = files.map(createStagedFile);
- setStagedFiles((prev) => [...prev, ...newStagedFiles]);
-
- try {
- // Upload each file using existing upload logic
- for (const stagedFile of newStagedFiles) {
- setStagedFiles((prev) =>
- prev.map((f) =>
- f.id === stagedFile.id ? { ...f, status: "uploading" } : f,
- ),
- );
-
- const formData = new FormData();
- formData.append("file", stagedFile.file);
- formData.append("chatId", chatId);
-
- const response = await fetch("/api/files/upload", {
- method: "POST",
- body: formData,
- });
-
- if (!response.ok) throw new Error("Upload failed");
-
- const data = await response.json();
-
- // Add to attachments on successful upload
- setAttachments((current) => [
- ...current,
- {
- url: data.url,
- name: stagedFile.file.name,
- contentType: stagedFile.file.type,
- path: data.path,
- },
- ]);
-
- // Mark as complete and remove from staged files
- setStagedFiles((prev) =>
- prev.map((f) =>
- f.id === stagedFile.id ? { ...f, status: "complete" } : f,
- ),
- );
- removeStagedFile(stagedFile.id);
- }
+ const handleDrop = useCallback(async (e: React.DragEvent) => {
+ e.preventDefault();
+ const files = Array.from(e.dataTransfer.files);
+ for (const file of files) {
+ await handleFileUpload(file);
+ }
+ }, []);
- toast.success("Files uploaded successfully");
- } catch (error) {
- console.error("Error uploading files:", error);
- toast.error("Failed to upload one or more files");
+ const handleFileClick = () => {
+ fileInputRef.current?.click();
+ };
- // Mark failed files
- newStagedFiles.forEach((file) => {
- setStagedFiles((prev) =>
- prev.map((f) =>
- f.id === file.id ? { ...f, status: "error" } : f,
- ),
- );
- });
- }
- }
- },
- [chatId, createStagedFile, removeStagedFile, setAttachments],
- );
+ const handleSuggestedAction = useCallback((action: string) => {
+ setInput(action);
+ // Trigger form submission with the suggested action
+ const formEvent = new Event('submit', { cancelable: true }) as unknown as React.FormEvent;
+ handleSubmit(formEvent);
+ }, [setInput, handleSubmit]);
return (
-
- {isLoading && expectingText && (
-
- )}
-
- {messages.length === 0 &&
- attachments.length === 0 &&
- stagedFiles.length === 0 && (
-
- {suggestedActions.map((suggestedAction, index) => (
- 1 ? "hidden sm:block" : "block")}
- >
- handleSuggestedAction(suggestedAction.action)}
- className="text-left border rounded-xl px-4 py-3.5 text-sm w-full h-auto flex flex-col items-start gap-1 transition-colors hover:bg-muted/80"
- >
-
- {suggestedAction.title}
-
-
- {suggestedAction.label}
-
-
-
- ))}
-
- )}
-
+
- {(attachments.length > 0 || stagedFiles.length > 0) && (
-
- {stagedFiles.map((stagedFile) => (
-
-
removeStagedFile(stagedFile.id)}
- />
- {stagedFile.status === "error" && (
-
-
- Upload failed
-
-
- )}
-
+ {messages.length === 0 && attachments.length === 0 && (
+
+ {suggestedActions.map((suggestedAction, index) => (
+ 1 ? "hidden sm:block" : "block")}
+ >
+ handleSuggestedAction(suggestedAction.action)}
+ className="text-left border rounded-xl px-4 py-3.5 text-sm w-full h-auto flex flex-col items-start gap-1 transition-colors hover:bg-muted/80"
+ >
+
+ {suggestedAction.title}
+
+
+ {suggestedAction.label}
+
+
+
))}
+
+ )}
- {attachments.map((attachment) => (
-
-
- setAttachments((current) =>
- current.filter((a) => a.url !== attachment.url)
- )
- }
- />
-
+ {attachments.length > 0 && (
+
+ {attachments.map((attachment, index) => (
+
{
+ setAttachments(prev => prev.filter((_, i) => i !== index));
+ }}
+ />
))}
)}
-
);
}
diff --git a/components/custom/overview.tsx b/components/custom/overview.tsx
index ac7f693..d6133e1 100644
--- a/components/custom/overview.tsx
+++ b/components/custom/overview.tsx
@@ -1,118 +1,62 @@
-import { motion } from "framer-motion";
-import Link from "next/link";
-import Image from "next/image";
+"use client";
+
import { useEffect, useState } from "react";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import { cn } from "@/lib/utils";
-const quotes = [
- "Building bridges in the Web3 ecosystem, one transaction at a time",
- "Empowering developers with seamless blockchain integration",
- "Simplifying complexity in the world of decentralized applications",
- "Where innovation meets blockchain technology",
- "Your trusted companion in the blockchain journey",
-];
+interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+}
-export const Overview = () => {
- const [currentQuote, setCurrentQuote] = useState("");
+export function Overview({ messages = [] }: { messages?: Message[] }) {
+ const [mounted, setMounted] = useState(false);
useEffect(() => {
- const updateQuote = () => {
- const randomIndex = Math.floor(Math.random() * quotes.length);
- setCurrentQuote(quotes[randomIndex]);
- };
-
- updateQuote();
- const interval = setInterval(updateQuote, 5 * 60 * 60 * 1000);
-
- return () => clearInterval(interval);
+ setMounted(true);
}, []);
- return (
-
-
-
-
-
-
-
-
Elron
-
- Powered by{" "}
-
- chainable.co
-
-
-
-
-
-
- “{currentQuote}”
-
-
-
-
- Welcome to Chainable Chat Bot - your AI-powered Web3 assistant.
- Built with Next.js and the latest Web3 technologies, this chatbot
- helps you interact with blockchain data and perform crypto
- operations seamlessly.
-
-
- Connect your wallet to access personalized features like balance
- checks, transaction history, and smart contract interactions.
+ if (!mounted) return null;
+
+ if (messages.length === 0) {
+ return (
+
+
+
+ Welcome to Elron
+
+
+ This is an AI chatbot that integrates with blockchain technologies.
-
- Powered by{" "}
-
- Base
- {" "}
- and secured with{" "}
-
- Supabase
-
+
+ You can start a conversation below.
+
+ );
+ }
-
+ {messages.map((message) => (
+
-
-
-
-
+
+
+
+ {message.content}
+
+
+
+
+ ))}
+
);
-};
+}
diff --git a/components/custom/run-python-code.tsx b/components/custom/run-python-code.tsx
new file mode 100644
index 0000000..7fcd2fe
--- /dev/null
+++ b/components/custom/run-python-code.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import { Code } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Textarea } from '@/components/ui/textarea';
+import { BetterTooltip } from '@/components/ui/tooltip';
+
+interface RunPythonCodeProps {
+ onRunCode: (code: string) => void;
+}
+
+export function RunPythonCode({ onRunCode }: RunPythonCodeProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [code, setCode] = useState('');
+
+ const handleRunCode = () => {
+ if (code.trim()) {
+ onRunCode(code.trim());
+ setCode('');
+ setIsOpen(false);
+ }
+ };
+
+ return (
+
+
+ setIsOpen(!isOpen)}
+ >
+
+ Run Python Code
+
+
+ {isOpen && (
+
+ setCode(e.target.value)}
+ className="mb-2"
+ rows={5}
+ />
+
+ Run Code
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/settings-dialog.tsx b/components/custom/settings-dialog.tsx
new file mode 100644
index 0000000..5dea598
--- /dev/null
+++ b/components/custom/settings-dialog.tsx
@@ -0,0 +1,219 @@
+"use client";
+
+import { useState } from "react";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
+import { Slider } from "@/components/ui/slider";
+import { Textarea } from "@/components/ui/textarea";
+import { Switch } from "@/components/ui/switch";
+import { toast } from "sonner";
+import { useModelSettings } from "@/lib/store/model-settings";
+import { useSettingsStore } from "@/lib/store/settings-store";
+import { BetterTooltip } from "@/components/ui/tooltip";
+import { SettingsIcon } from "./icons";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+
+export function SettingsDialog() {
+ const [isOpen, setIsOpen] = useState(false);
+ const modelSettings = useModelSettings();
+ const { openAIKey, setOpenAIKey, clearOpenAIKey, isLocalhost } = useSettingsStore();
+
+ // Tool-specific settings
+ const [fileSystemEnabled, setFileSystemEnabled] = useState(true);
+ const [allowedPaths, setAllowedPaths] = useState("/tmp,/downloads");
+ const [databaseEnabled, setDatabaseEnabled] = useState(true);
+ const [allowedTables, setAllowedTables] = useState("public.*");
+ const [summaryMaxLength, setSummaryMaxLength] = useState(500);
+
+ const handleSave = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (openAIKey && !openAIKey.startsWith('sk-')) {
+ toast.error("OpenAI API key should start with 'sk-'");
+ return;
+ }
+
+ // Save tool settings
+ localStorage.setItem('tool-settings', JSON.stringify({
+ fileSystem: {
+ enabled: fileSystemEnabled,
+ allowedPaths: allowedPaths.split(',').map(p => p.trim())
+ },
+ database: {
+ enabled: databaseEnabled,
+ allowedTables: allowedTables.split(',').map(t => t.trim())
+ },
+ summary: {
+ maxLength: summaryMaxLength
+ }
+ }));
+
+ toast.success("Settings saved successfully");
+ setIsOpen(false);
+ };
+
+ const handleClear = () => {
+ clearOpenAIKey();
+ toast.success("API key cleared - using default key");
+ };
+
+ return (
+
+
+
+
+
+ Settings
+
+
+
+
+
+ Settings
+
+
+
+
+ General
+ Tools
+ Model
+
+
+
+ {isLocalhost && (
+ <>
+
+
OpenAI API Key
+
+ setOpenAIKey(e.target.value)}
+ placeholder="sk-..."
+ className="font-mono flex-1"
+ />
+
+ Clear
+
+
+
+ Override the default OpenAI API key (localhost only)
+
+
+
+ >
+ )}
+
+
+
+ {/* File System Tool Settings */}
+
+
+ File System Access
+
+
+
setAllowedPaths(e.target.value)}
+ placeholder="Comma-separated allowed paths"
+ disabled={!fileSystemEnabled}
+ />
+
+ Specify comma-separated paths that the file system tool can access
+
+
+
+
+
+ {/* Database Tool Settings */}
+
+
+ Database Access
+
+
+
setAllowedTables(e.target.value)}
+ placeholder="Comma-separated allowed tables"
+ disabled={!databaseEnabled}
+ />
+
+ Specify comma-separated tables that the database tool can access
+
+
+
+
+
+ {/* Summary Tool Settings */}
+
+
Maximum Summary Length ({summaryMaxLength})
+
setSummaryMaxLength(value)}
+ min={100}
+ max={2000}
+ step={100}
+ />
+
+ Maximum length of generated summaries in characters
+
+
+
+
+
+
+ Temperature ({modelSettings.settings.temperature})
+
+ modelSettings.updateSettings({ temperature: value })
+ }
+ min={0}
+ max={2}
+ step={0.1}
+ />
+
+
+
+ System Prompt
+
+ modelSettings.updateSettings({ systemPrompt: e.target.value })
+ }
+ rows={3}
+ />
+
+
+
+
+
+ setIsOpen(false)}>
+ Cancel
+
+ Save Changes
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/summarize.tsx b/components/custom/summarize.tsx
new file mode 100644
index 0000000..f13fa04
--- /dev/null
+++ b/components/custom/summarize.tsx
@@ -0,0 +1,84 @@
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { SummarizeIcon } from './icons';
+
+interface SummarizeToolProps {
+ type: 'text' | 'document' | 'conversation';
+ args: {
+ content: string;
+ options?: {
+ maxLength?: number;
+ format?: 'bullet' | 'paragraph';
+ focus?: string;
+ };
+ };
+ result?: {
+ success: boolean;
+ summary?: string;
+ keyPoints?: string[];
+ error?: string;
+ };
+}
+
+export function SummarizeTool({ type, args, result }: SummarizeToolProps) {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const getActionText = () => {
+ switch (type) {
+ case 'text':
+ return 'Summarizing text';
+ case 'document':
+ return 'Summarizing document';
+ case 'conversation':
+ return 'Summarizing conversation';
+ default:
+ return 'Processing';
+ }
+ };
+
+ if (!result) {
+ return (
+
+
+
+
+
+
{getActionText()}
+
+
⋯
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {result.success ? (
+ <>
+ {result.summary && (
+
+ )}
+ {result.keyPoints && (
+
+
+ {result.keyPoints.map((point, index) => (
+ {point}
+ ))}
+
+
+ )}
+ >
+ ) : (
+
{result.error}
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/wallet-button.tsx b/components/custom/wallet-button.tsx
new file mode 100644
index 0000000..c158b70
--- /dev/null
+++ b/components/custom/wallet-button.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { WalletIcon } from 'lucide-react';
+import { ConnectButton } from '@rainbow-me/rainbowkit';
+import { Button } from '@/components/ui/button';
+import { BetterTooltip } from '@/components/ui/tooltip';
+
+export function WalletButton() {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return (
+
+
+ Connect Wallet
+
+ );
+ }
+
+ return (
+
+
+
+
+ Connect Wallet
+
+
+ {({
+ account,
+ chain,
+ openConnectModal,
+ openAccountModal,
+ mounted: rainbowKitMounted,
+ }) => {
+ const ready = rainbowKitMounted;
+ const connected = ready && account && chain;
+
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ connected ? openAccountModal() : openConnectModal();
+ }
+ }}
+ />
+ );
+ }}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/custom/web-search.tsx b/components/custom/web-search.tsx
new file mode 100644
index 0000000..b850c9a
--- /dev/null
+++ b/components/custom/web-search.tsx
@@ -0,0 +1,53 @@
+import React, { useState } from 'react';
+import { Search } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { BetterTooltip } from '@/components/ui/tooltip';
+
+interface WebSearchProps {
+ onSearch: (query: string) => void;
+}
+
+export function WebSearch({ onSearch }: WebSearchProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [query, setQuery] = useState('');
+
+ const handleSearch = () => {
+ if (query.trim()) {
+ onSearch(query.trim());
+ setQuery('');
+ setIsOpen(false);
+ }
+ };
+
+ return (
+
+
+
setIsOpen(!isOpen)}
+ >
+
+ Web Search
+
+ {isOpen && (
+
+ setQuery(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
+ className="flex-grow"
+ />
+
+ Search
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/providers/rainbow-kit-provider.tsx b/components/providers/rainbow-kit-provider.tsx
new file mode 100644
index 0000000..e50d728
--- /dev/null
+++ b/components/providers/rainbow-kit-provider.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { RainbowKitProvider as RainbowKitProviderBase } from '@rainbow-me/rainbowkit';
+import { darkTheme, lightTheme } from '@rainbow-me/rainbowkit';
+import '@rainbow-me/rainbowkit/styles.css';
+
+const customDarkTheme = darkTheme({
+ accentColor: 'hsl(var(--primary))',
+ accentColorForeground: 'hsl(var(--primary-foreground))',
+ borderRadius: 'medium',
+ overlayBlur: 'small',
+});
+
+const customLightTheme = lightTheme({
+ accentColor: 'hsl(var(--primary))',
+ accentColorForeground: 'hsl(var(--primary-foreground))',
+ borderRadius: 'medium',
+ overlayBlur: 'small',
+});
+
+export function RainbowKitProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/components/providers/wagmi-provider.tsx b/components/providers/wagmi-provider.tsx
new file mode 100644
index 0000000..70a15b8
--- /dev/null
+++ b/components/providers/wagmi-provider.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { WagmiProvider as WagmiProviderBase } from 'wagmi';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { http, createConfig } from 'wagmi';
+import { mainnet, base, baseSepolia } from 'wagmi/chains';
+
+const config = createConfig({
+ chains: [mainnet, base, baseSepolia],
+ transports: {
+ [mainnet.id]: http(),
+ [base.id]: http(),
+ [baseSepolia.id]: http(),
+ },
+});
+
+const queryClient = new QueryClient();
+
+export function WagmiProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 0000000..9fced93
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+import { type ThemeProviderProps } from "next-themes/dist/types";
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children} ;
+}
\ No newline at end of file
diff --git a/components/ui/button.test.tsx b/components/ui/button.test.tsx
deleted file mode 100644
index 21e8059..0000000
--- a/components/ui/button.test.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { render, screen } from "@/__tests__/test-utils";
-import { Button } from "./button";
-
-describe("Button Component", () => {
- it("renders correctly", () => {
- render(Click Me );
- const button = screen.getByText("Click Me");
- expect(button).toBeInTheDocument();
- });
-});
diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..54b87cd
--- /dev/null
+++ b/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx
new file mode 100644
index 0000000..c31c2b3
--- /dev/null
+++ b/components/ui/slider.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
+
+import { cn } from "@/lib/utils"
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+))
+Slider.displayName = SliderPrimitive.Root.displayName
+
+export { Slider }
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 0000000..69dcf09
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as React from "react";
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+import { cn } from "@/lib/utils";
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
+
+export { Switch };
\ No newline at end of file
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000..ce98f22
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+import { cn } from "@/lib/utils";
+
+const Tabs = TabsPrimitive.Root;
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
\ No newline at end of file
diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/components/ui/toaster.tsx
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/ui/use-toast.ts b/components/ui/use-toast.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/components/ui/use-toast.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/db/auth.ts b/db/auth.ts
index 72871df..896b40d 100644
--- a/db/auth.ts
+++ b/db/auth.ts
@@ -6,51 +6,86 @@ export type AuthError = {
};
export async function signIn(email: string, password: string) {
- const supabase = createClient();
+ try {
+ const supabase = createClient();
+ console.log("Attempting to sign in with email:", email);
- const { data, error } = await supabase.auth.signInWithPassword({
- email,
- password,
- });
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
- if (error) {
+ if (error) {
+ console.error("Sign in error:", error);
+ throw {
+ message: error.message || "Failed to sign in",
+ status: error.status || 500,
+ } as AuthError;
+ }
+
+ console.log("Sign in successful");
+ return data;
+ } catch (error: any) {
+ console.error("Unexpected sign in error:", error);
throw {
- message: error.message,
+ message: error.message || "An unexpected error occurred",
status: error.status || 500,
} as AuthError;
}
-
- return data;
}
export async function signUp(email: string, password: string) {
- const supabase = createClient();
+ try {
+ const supabase = createClient();
+ console.log("Attempting to sign up with email:", email);
+
+ const { data, error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ emailRedirectTo: `${location.origin}/auth/callback`,
+ },
+ });
- const { data, error } = await supabase.auth.signUp({
- email,
- password,
- options: {
- emailRedirectTo: `${location.origin}/auth/callback`,
- },
- });
+ if (error) {
+ console.error("Sign up error:", error);
+ throw {
+ message: error.message || "Failed to sign up",
+ status: error.status || 500,
+ } as AuthError;
+ }
- if (error) {
+ console.log("Sign up successful");
+ return data;
+ } catch (error: any) {
+ console.error("Unexpected sign up error:", error);
throw {
- message: error.message,
+ message: error.message || "An unexpected error occurred",
status: error.status || 500,
} as AuthError;
}
-
- return data;
}
export async function signOut() {
- const supabase = createClient();
- const { error } = await supabase.auth.signOut();
+ try {
+ const supabase = createClient();
+ console.log("Attempting to sign out");
+
+ const { error } = await supabase.auth.signOut();
+
+ if (error) {
+ console.error("Sign out error:", error);
+ throw {
+ message: error.message || "Failed to sign out",
+ status: error.status || 500,
+ } as AuthError;
+ }
- if (error) {
+ console.log("Sign out successful");
+ } catch (error: any) {
+ console.error("Unexpected sign out error:", error);
throw {
- message: error.message,
+ message: error.message || "An unexpected error occurred",
status: error.status || 500,
} as AuthError;
}
diff --git a/db/schema.sql b/db/schema.sql
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/db/schema.sql
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/elron-ai.code-workspace b/elron-ai.code-workspace
new file mode 100644
index 0000000..d2b1776
--- /dev/null
+++ b/elron-ai.code-workspace
@@ -0,0 +1,14 @@
+{
+ "folders": [
+ {
+ "path": "."
+ },
+ {
+ "path": "../../PycharmProjects/erlon-ai-workspace/nextjs-fastapi"
+ },
+ {
+ "path": "../cdp-wallet"
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/hooks/use-local-storage.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hooks/use-window-size.ts b/hooks/use-window-size.ts
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/hooks/use-window-size.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hooks/useWalletState.ts b/hooks/useWalletState.ts
index b8e154a..16fa0aa 100644
--- a/hooks/useWalletState.ts
+++ b/hooks/useWalletState.ts
@@ -1,3 +1,5 @@
+"use client";
+
import { useAccount, useChainId, useWalletClient } from "wagmi";
import { useEffect, useMemo } from "react";
import { toast } from "sonner";
diff --git a/lib/hooks/use-chat.ts b/lib/hooks/use-chat.ts
new file mode 100644
index 0000000..70fe16c
--- /dev/null
+++ b/lib/hooks/use-chat.ts
@@ -0,0 +1,35 @@
+import { useCallback } from 'react';
+import { useSettingsStore } from '../store/settings-store';
+import { useModelSettings } from '../store/model-settings';
+
+export function useChat() {
+ const { openAIKey } = useSettingsStore();
+ const { settings } = useModelSettings();
+
+ const sendMessage = useCallback(async (message: string) => {
+ try {
+ const response = await fetch('/api/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(openAIKey && { 'X-OpenAI-Key': openAIKey }),
+ },
+ body: JSON.stringify({
+ message,
+ settings,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to send message');
+ }
+
+ return response;
+ } catch (error) {
+ console.error('Error sending message:', error);
+ throw error;
+ }
+ }, [openAIKey, settings]);
+
+ return { sendMessage };
+}
\ No newline at end of file
diff --git a/lib/hooks/use-model-sync.ts b/lib/hooks/use-model-sync.ts
new file mode 100644
index 0000000..6e67052
--- /dev/null
+++ b/lib/hooks/use-model-sync.ts
@@ -0,0 +1,33 @@
+import { useEffect } from 'react';
+import { useModelStore } from '../store/model-store';
+import { useSettingsStore } from '../store/settings-store';
+import { toast } from 'sonner';
+
+export function useModelSync(selectedModelId?: string) {
+ const { models, selectedModelId: currentModelId, setSelectedModel } = useModelStore();
+ const { isLocalhost } = useSettingsStore();
+
+ useEffect(() => {
+ // Sync model selection if provided
+ if (selectedModelId && selectedModelId !== currentModelId) {
+ const newModel = models.find(m => m.id === selectedModelId);
+ if (newModel) {
+ // Check if model is available based on environment
+ const isOllamaModel = newModel.provider === 'ollama';
+ if (isOllamaModel && !isLocalhost) {
+ toast.error('Ollama models are only available in local development');
+ return;
+ }
+
+ setSelectedModel(selectedModelId);
+ toast.success(`Using ${newModel.name}${isOllamaModel ? ' (Local)' : ''}`);
+ }
+ }
+ }, [selectedModelId, currentModelId, models, setSelectedModel, isLocalhost]);
+
+ return {
+ currentModelId,
+ models,
+ setSelectedModel
+ };
+}
\ No newline at end of file
diff --git a/lib/hooks/use-model.ts b/lib/hooks/use-model.ts
new file mode 100644
index 0000000..474cd1f
--- /dev/null
+++ b/lib/hooks/use-model.ts
@@ -0,0 +1,55 @@
+import { useCallback } from 'react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'sonner';
+import { useModelStore } from '@/lib/store/model-store';
+
+export function useModel() {
+ const router = useRouter();
+ const {
+ models,
+ selectedModelId,
+ isLoading,
+ setModels,
+ setSelectedModel,
+ setIsLoading
+ } = useModelStore();
+
+ const selectedModel = models.find(model => model.id === selectedModelId) || models[0];
+
+ const handleModelChange = useCallback((value: string) => {
+ const newModel = models.find(m => m.id === value);
+ if (newModel) {
+ toast.success(
+ `Switching to ${newModel.name}${newModel.provider === 'ollama' ? ' (Local)' : ''}...`,
+ {
+ duration: 1000,
+ onAutoClose: () => {
+ setSelectedModel(value);
+ router.push(`/?model=${value}`);
+ }
+ }
+ );
+ }
+ }, [models, router, setSelectedModel]);
+
+ const fetchModels = useCallback(async () => {
+ try {
+ const response = await fetch('/api/models');
+ const data = await response.json();
+ setModels(data.models);
+ } catch (error) {
+ console.error('Error fetching models:', error);
+ toast.error('Failed to load available models');
+ }
+ }, [setModels]);
+
+ return {
+ models,
+ selectedModel,
+ selectedModelId,
+ isLoading,
+ handleModelChange,
+ fetchModels,
+ setIsLoading
+ };
+}
\ No newline at end of file
diff --git a/lib/openai-stream.ts b/lib/openai-stream.ts
new file mode 100644
index 0000000..ad22066
--- /dev/null
+++ b/lib/openai-stream.ts
@@ -0,0 +1,51 @@
+import { createParser } from 'eventsource-parser';
+
+export async function OpenAIStream(response: Response) {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ throw new Error(JSON.stringify(error));
+ }
+
+ return new ReadableStream({
+ async start(controller) {
+ const parser = createParser((event) => {
+ if (event.type === 'event') {
+ try {
+ const data = JSON.parse(event.data);
+ const text = data.choices[0]?.delta?.content || '';
+
+ if (text) {
+ controller.enqueue(encoder.encode(text));
+ }
+ } catch (e) {
+ console.error('Parse error:', e);
+ }
+ }
+ });
+
+ // Stream the response
+ const reader = response.body?.getReader();
+ if (!reader) {
+ controller.close();
+ return;
+ }
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ controller.close();
+ break;
+ }
+ parser.feed(decoder.decode(value));
+ }
+ } catch (e) {
+ console.error('Stream error:', e);
+ controller.error(e);
+ }
+ },
+ });
+}
\ No newline at end of file
diff --git a/lib/rainbowkit-theme.ts b/lib/rainbowkit-theme.ts
new file mode 100644
index 0000000..a7847e2
--- /dev/null
+++ b/lib/rainbowkit-theme.ts
@@ -0,0 +1,15 @@
+import { darkTheme, lightTheme } from '@rainbow-me/rainbowkit';
+
+export const customDarkTheme = darkTheme({
+ accentColor: 'hsl(var(--primary))',
+ accentColorForeground: 'hsl(var(--primary-foreground))',
+ borderRadius: 'medium',
+ overlayBlur: 'small',
+});
+
+export const customLightTheme = lightTheme({
+ accentColor: 'hsl(var(--primary))',
+ accentColorForeground: 'hsl(var(--primary-foreground))',
+ borderRadius: 'medium',
+ overlayBlur: 'small',
+});
\ No newline at end of file
diff --git a/lib/store/model-settings.ts b/lib/store/model-settings.ts
new file mode 100644
index 0000000..f75130c
--- /dev/null
+++ b/lib/store/model-settings.ts
@@ -0,0 +1,40 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+interface ModelSettings {
+ temperature: number;
+ topK: number;
+ topP: number;
+ repeatPenalty: number;
+ systemPrompt: string;
+}
+
+interface ModelSettingsState {
+ settings: ModelSettings;
+ updateSettings: (settings: Partial) => void;
+ resetSettings: () => void;
+}
+
+const DEFAULT_SETTINGS: ModelSettings = {
+ temperature: 0.7,
+ topK: 40,
+ topP: 0.9,
+ repeatPenalty: 1.1,
+ systemPrompt: "You are a helpful AI assistant. You are direct and concise in your responses.",
+};
+
+export const useModelSettings = create()(
+ persist(
+ (set) => ({
+ settings: DEFAULT_SETTINGS,
+ updateSettings: (newSettings) =>
+ set((state) => ({
+ settings: { ...state.settings, ...newSettings },
+ })),
+ resetSettings: () => set({ settings: DEFAULT_SETTINGS }),
+ }),
+ {
+ name: 'model-settings-storage',
+ }
+ )
+);
\ No newline at end of file
diff --git a/lib/store/model-store.ts b/lib/store/model-store.ts
new file mode 100644
index 0000000..5abd6ae
--- /dev/null
+++ b/lib/store/model-store.ts
@@ -0,0 +1,33 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+interface Model {
+ id: string;
+ name: string;
+ provider: 'openai' | 'ollama';
+}
+
+interface ModelState {
+ models: Model[];
+ selectedModelId: string;
+ setModels: (models: Model[]) => void;
+ setSelectedModel: (modelId: string) => void;
+ isLoading: boolean;
+ setIsLoading: (loading: boolean) => void;
+}
+
+export const useModelStore = create()(
+ persist(
+ (set) => ({
+ models: [],
+ selectedModelId: 'gpt-3.5-turbo',
+ isLoading: true,
+ setModels: (models) => set({ models, isLoading: false }),
+ setSelectedModel: (modelId) => set({ selectedModelId: modelId }),
+ setIsLoading: (loading) => set({ isLoading: loading }),
+ }),
+ {
+ name: 'model-store',
+ }
+ )
+);
\ No newline at end of file
diff --git a/lib/store/settings-store.ts b/lib/store/settings-store.ts
new file mode 100644
index 0000000..a380fae
--- /dev/null
+++ b/lib/store/settings-store.ts
@@ -0,0 +1,25 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+interface SettingsState {
+ openAIKey: string;
+ setOpenAIKey: (key: string) => void;
+ clearOpenAIKey: () => void;
+ isLocalhost: boolean;
+ setIsLocalhost: (isLocal: boolean) => void;
+}
+
+export const useSettingsStore = create()(
+ persist(
+ (set) => ({
+ openAIKey: '',
+ isLocalhost: typeof window !== 'undefined' && window.location.hostname === 'localhost',
+ setOpenAIKey: (key) => set({ openAIKey: key }),
+ clearOpenAIKey: () => set({ openAIKey: '' }),
+ setIsLocalhost: (isLocal) => set({ isLocalhost: isLocal }),
+ }),
+ {
+ name: 'settings-storage',
+ }
+ )
+);
\ No newline at end of file
diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts
index b7af712..7008726 100644
--- a/lib/supabase/client.ts
+++ b/lib/supabase/client.ts
@@ -2,8 +2,33 @@ import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "./types";
-export const createClient = () =>
- createBrowserClient(
+export const createClient = () => {
+ return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ const cookie = document.cookie
+ .split("; ")
+ .find((row) => row.startsWith(`${name}=`));
+ return cookie ? decodeURIComponent(cookie.split("=")[1]) : undefined;
+ },
+ set(name: string, value: string, options: { path: string; maxAge?: number; domain?: string; sameSite?: "lax" | "strict" | "none"; secure?: boolean }) {
+ let cookie = `${name}=${encodeURIComponent(value)}`;
+ if (options.maxAge) cookie += `; Max-Age=${options.maxAge}`;
+ if (options.path) cookie += `; Path=${options.path}`;
+ if (options.domain) cookie += `; Domain=${options.domain}`;
+ if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
+ if (options.secure) cookie += "; Secure";
+ document.cookie = cookie;
+ },
+ remove(name: string, options: { path: string; domain?: string }) {
+ document.cookie = `${name}=; Path=${options.path}; Max-Age=0${
+ options.domain ? `; Domain=${options.domain}` : ""
+ }`;
+ },
+ },
+ }
);
+};
diff --git a/lib/supabase/middleware.ts b/lib/supabase/middleware.ts
index b0c4d1e..3ddfc0b 100644
--- a/lib/supabase/middleware.ts
+++ b/lib/supabase/middleware.ts
@@ -14,42 +14,47 @@ export const updateSession = async (request: NextRequest) => {
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
- getAll() {
- return request.cookies.getAll();
+ get(name: string) {
+ return request.cookies.get(name)?.value;
},
- setAll(cookiesToSet) {
- cookiesToSet.forEach(({ name, value }) =>
- request.cookies.set(name, value),
- );
- response = NextResponse.next({
- request,
- });
- cookiesToSet.forEach(({ name, value, options }) =>
- response.cookies.set(name, value, options),
- );
+ set(name: string, value: string, options: { path: string; maxAge?: number; domain?: string; sameSite?: "lax" | "strict" | "none"; secure?: boolean }) {
+ try {
+ request.cookies.set({
+ name,
+ value,
+ ...options,
+ });
+ response.cookies.set({
+ name,
+ value,
+ ...options,
+ });
+ } catch (error) {
+ console.error("Error setting cookie in middleware:", error);
+ }
+ },
+ remove(name: string, options: { path: string; domain?: string }) {
+ try {
+ request.cookies.delete(name);
+ response.cookies.set({
+ name,
+ value: "",
+ ...options,
+ maxAge: 0,
+ });
+ } catch (error) {
+ console.error("Error removing cookie in middleware:", error);
+ }
},
},
},
);
- const user = await supabase.auth.getUser();
-
- // Protected routes
- if (request.nextUrl.pathname === "/" && user.error) {
- return NextResponse.redirect(new URL("/register", request.url));
- }
-
- // Redirect logged in users from auth pages
- if (
- (request.nextUrl.pathname === "/login" ||
- request.nextUrl.pathname === "/register") &&
- !user.error
- ) {
- return NextResponse.redirect(new URL("/", request.url));
- }
+ await supabase.auth.getSession();
return response;
- } catch (e) {
+ } catch (error) {
+ console.error("Error in updateSession middleware:", error);
return NextResponse.next({
request: {
headers: request.headers,
diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts
index 03eaf52..b13e6b6 100644
--- a/lib/supabase/server.ts
+++ b/lib/supabase/server.ts
@@ -4,7 +4,7 @@ import { cookies } from "next/headers";
import { Database } from "./types";
export const createClient = async () => {
- const cookieStore = await cookies();
+ const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
@@ -14,6 +14,22 @@ export const createClient = async () => {
get(name: string) {
return cookieStore.get(name)?.value;
},
+ set(name: string, value: string, options: { path: string; maxAge?: number; domain?: string; sameSite?: "lax" | "strict" | "none"; secure?: boolean }) {
+ try {
+ cookieStore.set({ name, value, ...options });
+ } catch (error) {
+ // Handle cookie setting errors
+ console.error("Error setting cookie:", error);
+ }
+ },
+ remove(name: string, options: { path: string; domain?: string }) {
+ try {
+ cookieStore.set({ name, value: "", ...options, maxAge: 0 });
+ } catch (error) {
+ // Handle cookie removal errors
+ console.error("Error removing cookie:", error);
+ }
+ },
},
},
);
diff --git a/next.config.js b/next.config.js
index d516d01..3459c12 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,186 +1,27 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
+ reactStrictMode: true,
+ compiler: {
+ styledComponents: true,
+ },
images: {
- domains: [
- "avatar.vercel.sh",
- "chainable.guru",
- "avatars.githubusercontent.com",
- "img.clerk.com",
- ],
remotePatterns: [
{
- protocol: "https",
- hostname: "**.public.blob.vercel-storage.com",
- pathname: "/**",
- },
- {
- protocol: "https",
- hostname: "**.vercel-storage.com",
- pathname: "/**",
- },
- {
- protocol: "https",
- hostname: "avatar.vercel.sh",
- pathname: "/**",
- },
- {
- protocol: "https",
- hostname: "avatars.githubusercontent.com",
- pathname: "/**",
- },
- {
- protocol: "https",
- hostname: "img.clerk.com",
- pathname: "/**",
- },
- {
- protocol: "https",
- hostname: "**.vercel.app",
- pathname: "/**",
- },
- // Add blockchain-specific patterns
- {
- protocol: "https",
- hostname: "**.opensea.io",
- pathname: "/**",
- },
- {
- protocol: "https",
- hostname: "**.nftstorage.link",
- pathname: "/**",
- },
- {
- protocol: "https",
- hostname: "ipfs.io",
- pathname: "/**",
+ protocol: 'https',
+ hostname: '**',
},
],
- // Configure local image handling
- dangerouslyAllowSVG: true,
- contentDispositionType: "attachment",
- contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
- // Optimize images
- deviceSizes: [640, 750, 828, 1080, 1200, 1920],
- imageSizes: [16, 32, 48, 64, 96, 128, 256],
- formats: ["image/webp", "image/avif"],
- minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
- // Allow local logos
- loader: "default",
- loaderFile: undefined,
- path: "/_next/image",
- disableStaticImages: false,
- unoptimized: process.env.NODE_ENV === "production",
- },
- // Other config
- typescript: {
- ignoreBuildErrors: true,
},
experimental: {
serverActions: {
- allowedOrigins: ["localhost:3000", "chainable.guru"],
- bodySizeLimit: "2mb",
+ allowedOrigins: ['localhost:3000'],
},
},
- // Add webpack configuration for handling local images
- webpack(config) {
- config.module.rules.push({
- test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
- issuer: /\.[jt]sx?$/,
- use: [
- {
- loader: "url-loader",
- options: {
- limit: 10000,
- name: "static/media/[name].[hash:8].[ext]",
- publicPath: "/_next",
- },
- },
- ],
- });
-
- return config;
- },
- // Add public directory handling
- async rewrites() {
- return [
- {
- source: "/favicon.ico",
- destination: "/public/favicon.ico",
- },
- {
- source: "/logos/:path*",
- destination: "/public/logos/:path*",
- },
- {
- source: "/api/search/:path*",
- destination: "https://api.duckduckgo.com/:path*",
- },
- {
- source: "/api/opensearch/:path*",
- destination: "https://api.bing.microsoft.com/:path*",
- },
- ];
- },
- // Add headers for cache control
- async headers() {
- return [
- {
- source: "/favicon.ico",
- headers: [
- {
- key: "Cache-Control",
- value: "public, max-age=31536000, immutable",
- },
- ],
- },
- {
- source: "/icon.svg",
- headers: [
- {
- key: "Cache-Control",
- value: "public, max-age=31536000, immutable",
- },
- ],
- },
- {
- source: "/api/search/:path*",
- headers: [
- {
- key: "Access-Control-Allow-Origin",
- value: "*",
- },
- ],
- },
- {
- source: "/(.*).(jpg|jpeg|png|webp|avif|ico|svg)",
- headers: [
- {
- key: "Cache-Control",
- value: "public, max-age=31536000, immutable",
- },
- ],
- },
- ];
+ typescript: {
+ ignoreBuildErrors: false,
},
- // Add webpack configuration for static files
- webpack(config) {
- config.module.rules.push({
- test: /\.(ico|png|jpe?g|gif|svg|webp|avif)$/i,
- issuer: /\.[jt]sx?$/,
- use: [
- {
- loader: "url-loader",
- options: {
- limit: 10000,
- name: "static/media/[name].[hash:8].[ext]",
- publicPath: "/_next",
- fallback: "file-loader",
- },
- },
- ],
- });
-
- return config;
+ eslint: {
+ ignoreDuringBuilds: false,
},
};
diff --git a/package.json b/package.json
index b898209..41783d1 100644
--- a/package.json
+++ b/package.json
@@ -3,11 +3,13 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
+ "dev": "kill -9 $(lsof -ti:3000) || true && next dev",
"build": "pnpm setup-favicons && next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
+ "test:ollama": "vitest run __tests__/components/ollama-chat.test.tsx",
+ "test:ollama:watch": "vitest __tests__/components/ollama-chat.test.tsx --watch",
"setup-favicons": "pnpm optimize-images && tsx scripts/setup-favicons.ts",
"optimize-images": "tsx scripts/optimize-images.ts",
"biome:clean": "npx biome format --write && npx biome lint --fix"
@@ -15,6 +17,8 @@
"dependencies": {
"@ai-sdk/openai": "^0.0.60",
"@coinbase/coinbase-sdk": "^0.10.0",
+ "@langchain/core": "^0.3.19",
+ "@langchain/openai": "^0.3.14",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
@@ -22,10 +26,13 @@
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-progress": "^1.1.0",
+ "@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
+ "@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
+ "@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@rainbow-me/rainbowkit": "^2.2.0",
@@ -46,13 +53,16 @@
"diff-match-patch": "^1.0.5",
"dotenv": "^16.4.5",
"ethers": "^6.13.4",
+ "eventsource-parser": "^3.0.0",
"framer-motion": "^11.11.17",
"geist": "^1.3.1",
"gpt3-tokenizer": "^1.1.5",
+ "langchain": "^0.3.6",
"lucide-react": "^0.446.0",
"nanoid": "^5.0.8",
"next": "15.0.4-canary.15",
"next-themes": "^0.3.0",
+ "openai-edge": "^1.2.2",
"orderedmap": "^2.1.1",
"prosemirror-example-setup": "^1.2.3",
"prosemirror-inputrules": "^1.4.0",
@@ -83,6 +93,7 @@
"@tailwindcss/typography": "^0.5.15",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
+ "@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/chalk": "^2.2.4",
"@types/d3-scale": "^4.0.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c5fa35b..af282b5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,12 @@ importers:
'@coinbase/coinbase-sdk':
specifier: ^0.10.0
version: 0.10.0(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8)
+ '@langchain/core':
+ specifier: ^0.3.19
+ version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
+ '@langchain/openai':
+ specifier: ^0.3.14
+ version: 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
'@radix-ui/react-alert-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -35,18 +41,27 @@ importers:
'@radix-ui/react-progress':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-scroll-area':
+ specifier: ^1.2.1
+ version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-select':
specifier: ^2.1.2
version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-separator':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-slider':
+ specifier: ^1.1.2
+ version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot':
specifier: ^1.1.0
version: 1.1.0(@types/react@18.3.12)(react@18.2.0)
'@radix-ui/react-switch':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-tabs':
+ specifier: ^1.1.1
+ version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.1.4
version: 1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -79,7 +94,7 @@ importers:
version: 5.1.11(aaf5leqjl3fvobyhzmbv555pdm)
ai:
specifier: 3.4.33
- version: 3.4.33(react@18.2.0)(sswr@2.1.0(svelte@5.2.3))(svelte@5.2.3)(vue@3.5.13(typescript@5.6.3))(zod@3.23.8)
+ version: 3.4.33(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))(react@18.2.0)(sswr@2.1.0(svelte@5.2.3))(svelte@5.2.3)(vue@3.5.13(typescript@5.6.3))(zod@3.23.8)
chalk:
specifier: ^5.3.0
version: 5.3.0
@@ -107,6 +122,9 @@ importers:
ethers:
specifier: ^6.13.4
version: 6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ eventsource-parser:
+ specifier: ^3.0.0
+ version: 3.0.0
framer-motion:
specifier: ^11.11.17
version: 11.11.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -116,6 +134,9 @@ importers:
gpt3-tokenizer:
specifier: ^1.1.5
version: 1.1.5
+ langchain:
+ specifier: ^0.3.6
+ version: 0.3.6(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(axios@1.7.7)(encoding@0.1.13)(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
lucide-react:
specifier: ^0.446.0
version: 0.446.0(react@18.2.0)
@@ -128,6 +149,9 @@ importers:
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ openai-edge:
+ specifier: ^1.2.2
+ version: 1.2.2
orderedmap:
specifier: ^2.1.1
version: 2.1.1
@@ -213,6 +237,9 @@ importers:
'@testing-library/react':
specifier: ^16.0.1
version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@testing-library/react-hooks':
+ specifier: ^8.0.1
+ version: 8.0.1(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@testing-library/user-event':
specifier: ^14.5.2
version: 14.5.2(@testing-library/dom@10.4.0)
@@ -1704,6 +1731,22 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+ '@langchain/core@0.3.19':
+ resolution: {integrity: sha512-pJVOAHShefu1SRO8uhzUs0Pexah/Ib66WETLMScIC2w9vXlpwQy3DzXJPJ5X7ixry9N666jYO5cHtM2Z1DnQIQ==}
+ engines: {node: '>=18'}
+
+ '@langchain/openai@0.3.14':
+ resolution: {integrity: sha512-lNWjUo1tbvsss45IF7UQtMu1NJ6oUKvhgPYWXnX9f/d6OmuLu7D99HQ3Y88vLcUo9XjjOy417olYHignMduMjA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@langchain/core': '>=0.2.26 <0.4.0'
+
+ '@langchain/textsplitters@0.1.0':
+ resolution: {integrity: sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@langchain/core': '>=0.2.21 <0.4.0'
+
'@lit-labs/ssr-dom-shim@1.2.1':
resolution: {integrity: sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==}
@@ -2295,6 +2338,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-scroll-area@1.2.1':
+ resolution: {integrity: sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-select@2.1.2':
resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==}
peerDependencies:
@@ -2321,6 +2377,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-slider@1.2.1':
+ resolution: {integrity: sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-slot@1.1.0':
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
peerDependencies:
@@ -2343,6 +2412,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-tabs@1.1.1':
+ resolution: {integrity: sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-tooltip@1.1.4':
resolution: {integrity: sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==}
peerDependencies:
@@ -2829,6 +2911,22 @@ packages:
resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==}
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ '@testing-library/react-hooks@8.0.1':
+ resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ '@types/react': ^16.9.0 || ^17.0.0
+ react: ^16.9.0 || ^17.0.0
+ react-dom: ^16.9.0 || ^17.0.0
+ react-test-renderer: ^16.9.0 || ^17.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react-dom:
+ optional: true
+ react-test-renderer:
+ optional: true
+
'@testing-library/react@16.0.1':
resolution: {integrity: sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==}
engines: {node: '>=18'}
@@ -2930,9 +3028,15 @@ packages:
'@types/ms@0.7.34':
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
+ '@types/node-fetch@2.6.12':
+ resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
+
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
+ '@types/node@18.19.66':
+ resolution: {integrity: sha512-14HmtUdGxFUalGRfLLn9Gc1oNWvWh5zNbsyOLo5JV6WARSeN1QcEBKRnZm9QqNfrutgsl/hY4eJW63aZ44aBCg==}
+
'@types/node@20.17.6':
resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==}
@@ -2954,6 +3058,9 @@ packages:
'@types/react@18.3.12':
resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==}
+ '@types/retry@0.12.0':
+ resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
+
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
@@ -2970,6 +3077,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+ '@types/uuid@10.0.0':
+ resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
+
'@types/ws@8.5.13':
resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==}
@@ -3356,6 +3466,10 @@ packages:
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
engines: {node: '>= 14'}
+ agentkeepalive@4.5.0:
+ resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
+ engines: {node: '>= 8.0.0'}
+
ai@3.4.33:
resolution: {integrity: sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==}
engines: {node: '>=18'}
@@ -3861,6 +3975,10 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+ commander@10.0.1:
+ resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
+ engines: {node: '>=14'}
+
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
@@ -4494,6 +4612,9 @@ packages:
eventemitter2@6.4.9:
resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==}
+ eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@@ -4505,6 +4626,10 @@ packages:
resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==}
engines: {node: '>=14.18'}
+ eventsource-parser@3.0.0:
+ resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
+ engines: {node: '>=18.0.0'}
+
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@@ -4640,10 +4765,17 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
+ form-data-encoder@1.7.2:
+ resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
+
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'}
+ formdata-node@4.4.1:
+ resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
+ engines: {node: '>= 12.20'}
+
framer-motion@11.11.17:
resolution: {integrity: sha512-O8QzvoKiuzI5HSAHbcYuL6xU+ZLXbrH7C8Akaato4JzQbX2ULNeniqC2Vo5eiCtFktX9XsJ+7nUhxcl2E2IjpA==}
peerDependencies:
@@ -4888,6 +5020,9 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
+ humanize-ms@1.2.1:
+ resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+
i18next-browser-languagedetector@7.1.0:
resolution: {integrity: sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==}
@@ -5255,6 +5390,9 @@ packages:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
+ js-tiktoken@1.0.15:
+ resolution: {integrity: sha512-65ruOWWXDEZHHbAo7EjOcNxOGasQKbL4Fq3jEr2xsCqSsoOo6VVSqzWQb6PRIqypFSDcma4jO90YP0w5X8qVXQ==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -5331,6 +5469,10 @@ packages:
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
+ jsonpointer@5.0.1:
+ resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
+ engines: {node: '>=0.10.0'}
+
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@@ -5349,6 +5491,60 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
+ langchain@0.3.6:
+ resolution: {integrity: sha512-erZOIKXzwCOrQHqY9AyjkQmaX62zUap1Sigw1KrwMUOnVoLKkVNRmAyxFlNZDZ9jLs/58MaQcaT9ReJtbj3x6w==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@langchain/anthropic': '*'
+ '@langchain/aws': '*'
+ '@langchain/cohere': '*'
+ '@langchain/core': '>=0.2.21 <0.4.0'
+ '@langchain/google-genai': '*'
+ '@langchain/google-vertexai': '*'
+ '@langchain/groq': '*'
+ '@langchain/mistralai': '*'
+ '@langchain/ollama': '*'
+ axios: '*'
+ cheerio: '*'
+ handlebars: ^4.7.8
+ peggy: ^3.0.2
+ typeorm: '*'
+ peerDependenciesMeta:
+ '@langchain/anthropic':
+ optional: true
+ '@langchain/aws':
+ optional: true
+ '@langchain/cohere':
+ optional: true
+ '@langchain/google-genai':
+ optional: true
+ '@langchain/google-vertexai':
+ optional: true
+ '@langchain/groq':
+ optional: true
+ '@langchain/mistralai':
+ optional: true
+ '@langchain/ollama':
+ optional: true
+ axios:
+ optional: true
+ cheerio:
+ optional: true
+ handlebars:
+ optional: true
+ peggy:
+ optional: true
+ typeorm:
+ optional: true
+
+ langsmith@0.2.7:
+ resolution: {integrity: sha512-9LFOp30cQ9K/7rzMt4USBI0SEKKhsH4l42ZERBPXOmDXnR5gYpsGFw8SZR0A6YLnc6vvoEmtr/XKel0Odq2UWw==}
+ peerDependencies:
+ openai: '*'
+ peerDependenciesMeta:
+ openai:
+ optional: true
+
language-subtag-registry@0.3.23:
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
@@ -5813,6 +6009,10 @@ packages:
multiformats@9.9.0:
resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}
+ mustache@4.2.0:
+ resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
+ hasBin: true
+
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -5897,6 +6097,10 @@ packages:
resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==}
engines: {node: '>= 0.10.5'}
+ node-domexception@1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+
node-fetch-native@1.6.4:
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
@@ -6026,6 +6230,22 @@ packages:
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
engines: {node: '>=8'}
+ openai-edge@1.2.2:
+ resolution: {integrity: sha512-C3/Ao9Hkx5uBPv9YFBpX/x59XMPgPUU4dyGg/0J2sOJ7O9D98kD+lfdOc7v/60oYo5xzMGct80uFkYLH+X2qgw==}
+ engines: {node: '>=18'}
+
+ openai@4.73.1:
+ resolution: {integrity: sha512-nWImDJBcUsqrhy7yJScXB4+iqjzbUEgzfA3un/6UnHFdwWhjX24oztj69Ped/njABfOdLcO/F7CeWTI5dt8Xmg==}
+ hasBin: true
+ peerDependencies:
+ zod: ^3.23.8
+ peerDependenciesMeta:
+ zod:
+ optional: true
+
+ openapi-types@12.1.3:
+ resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -6041,6 +6261,10 @@ packages:
typescript:
optional: true
+ p-finally@1.0.0:
+ resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
+ engines: {node: '>=4'}
+
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
@@ -6061,6 +6285,18 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-queue@6.6.2:
+ resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
+ engines: {node: '>=8'}
+
+ p-retry@4.6.2:
+ resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
+ engines: {node: '>=8'}
+
+ p-timeout@3.2.0:
+ resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
+ engines: {node: '>=8'}
+
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
@@ -6412,6 +6648,12 @@ packages:
peerDependencies:
react: ^18.2.0
+ react-error-boundary@3.1.4:
+ resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==}
+ engines: {node: '>=10', npm: '>=6'}
+ peerDependencies:
+ react: '>=16.13.1'
+
react-error-boundary@4.1.2:
resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==}
peerDependencies:
@@ -7237,6 +7479,9 @@ packages:
uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+ undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
@@ -7395,6 +7640,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
+ uuid@10.0.0:
+ resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+ hasBin: true
+
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
@@ -7534,6 +7783,10 @@ packages:
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
+ web-streams-polyfill@4.0.0-beta.3:
+ resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
+ engines: {node: '>= 14'}
+
webauthn-p256@0.0.10:
resolution: {integrity: sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA==}
@@ -9239,6 +9492,37 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
+ '@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))':
+ dependencies:
+ ansi-styles: 5.2.0
+ camelcase: 6.3.0
+ decamelize: 1.2.0
+ js-tiktoken: 1.0.15
+ langsmith: 0.2.7(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
+ mustache: 4.2.0
+ p-queue: 6.6.2
+ p-retry: 4.6.2
+ uuid: 10.0.0
+ zod: 3.23.8
+ zod-to-json-schema: 3.23.5(zod@3.23.8)
+ transitivePeerDependencies:
+ - openai
+
+ '@langchain/openai@0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)':
+ dependencies:
+ '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
+ js-tiktoken: 1.0.15
+ openai: 4.73.1(encoding@0.1.13)(zod@3.23.8)
+ zod: 3.23.8
+ zod-to-json-schema: 3.23.5(zod@3.23.8)
+ transitivePeerDependencies:
+ - encoding
+
+ '@langchain/textsplitters@0.1.0(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))':
+ dependencies:
+ '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
+ js-tiktoken: 1.0.15
+
'@lit-labs/ssr-dom-shim@1.2.1': {}
'@lit/reactive-element@1.6.3':
@@ -9858,6 +10142,23 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
+ '@radix-ui/react-scroll-area@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@radix-ui/number': 1.1.0
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-select@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/number': 1.1.0
@@ -9896,6 +10197,25 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
+ '@radix-ui/react-slider@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@radix-ui/number': 1.1.0
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.2.0)
@@ -9918,6 +10238,22 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
+ '@radix-ui/react-tabs@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-tooltip@1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -10532,6 +10868,15 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
+ '@testing-library/react-hooks@8.0.1(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ react: 18.2.0
+ react-error-boundary: 3.1.4(react@18.2.0)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ react-dom: 18.2.0(react@18.2.0)
+
'@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.26.0
@@ -10635,10 +10980,19 @@ snapshots:
'@types/ms@0.7.34': {}
+ '@types/node-fetch@2.6.12':
+ dependencies:
+ '@types/node': 20.17.6
+ form-data: 4.0.1
+
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 20.17.6
+ '@types/node@18.19.66':
+ dependencies:
+ undici-types: 5.26.5
+
'@types/node@20.17.6':
dependencies:
undici-types: 6.19.8
@@ -10662,6 +11016,8 @@ snapshots:
'@types/prop-types': 15.7.13
csstype: 3.1.3
+ '@types/retry@0.12.0': {}
+
'@types/stack-utils@2.0.3': {}
'@types/testing-library__jest-dom@6.0.0':
@@ -10674,6 +11030,8 @@ snapshots:
'@types/unist@3.0.3': {}
+ '@types/uuid@10.0.0': {}
+
'@types/ws@8.5.13':
dependencies:
'@types/node': 20.17.6
@@ -11670,7 +12028,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ai@3.4.33(react@18.2.0)(sswr@2.1.0(svelte@5.2.3))(svelte@5.2.3)(vue@3.5.13(typescript@5.6.3))(zod@3.23.8):
+ agentkeepalive@4.5.0:
+ dependencies:
+ humanize-ms: 1.2.1
+
+ ai@3.4.33(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))(react@18.2.0)(sswr@2.1.0(svelte@5.2.3))(svelte@5.2.3)(vue@3.5.13(typescript@5.6.3))(zod@3.23.8):
dependencies:
'@ai-sdk/provider': 0.0.26
'@ai-sdk/provider-utils': 1.0.22(zod@3.23.8)
@@ -11686,6 +12048,7 @@ snapshots:
secure-json-parse: 2.7.0
zod-to-json-schema: 3.23.5(zod@3.23.8)
optionalDependencies:
+ openai: 4.73.1(encoding@0.1.13)(zod@3.23.8)
react: 18.2.0
sswr: 2.1.0(svelte@5.2.3)
svelte: 5.2.3
@@ -12267,6 +12630,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
+ commander@10.0.1: {}
+
commander@12.1.0: {}
commander@2.20.3: {}
@@ -13078,12 +13443,16 @@ snapshots:
eventemitter2@6.4.9: {}
+ eventemitter3@4.0.7: {}
+
eventemitter3@5.0.1: {}
events@3.3.0: {}
eventsource-parser@1.1.2: {}
+ eventsource-parser@3.0.0: {}
+
execa@5.1.1:
dependencies:
cross-spawn: 7.0.6
@@ -13230,12 +13599,19 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
+ form-data-encoder@1.7.2: {}
+
form-data@4.0.1:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
+ formdata-node@4.4.1:
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 4.0.0-beta.3
+
framer-motion@11.11.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
tslib: 2.8.1
@@ -13508,6 +13884,10 @@ snapshots:
human-signals@5.0.0: {}
+ humanize-ms@1.2.1:
+ dependencies:
+ ms: 2.1.3
+
i18next-browser-languagedetector@7.1.0:
dependencies:
'@babel/runtime': 7.26.0
@@ -13888,6 +14268,10 @@ snapshots:
joycon@3.1.1: {}
+ js-tiktoken@1.0.15:
+ dependencies:
+ base64-js: 1.5.1
+
js-tokens@4.0.0: {}
js-yaml@3.14.1:
@@ -13989,6 +14373,8 @@ snapshots:
chalk: 5.3.0
diff-match-patch: 1.0.5
+ jsonpointer@5.0.1: {}
+
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.8
@@ -14010,6 +14396,38 @@ snapshots:
kind-of@6.0.3: {}
+ langchain@0.3.6(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(axios@1.7.7)(encoding@0.1.13)(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)):
+ dependencies:
+ '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
+ '@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
+ '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))
+ js-tiktoken: 1.0.15
+ js-yaml: 4.1.0
+ jsonpointer: 5.0.1
+ langsmith: 0.2.7(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))
+ openapi-types: 12.1.3
+ p-retry: 4.6.2
+ uuid: 10.0.0
+ yaml: 2.6.1
+ zod: 3.23.8
+ zod-to-json-schema: 3.23.5(zod@3.23.8)
+ optionalDependencies:
+ axios: 1.7.7
+ transitivePeerDependencies:
+ - encoding
+ - openai
+
+ langsmith@0.2.7(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)):
+ dependencies:
+ '@types/uuid': 10.0.0
+ commander: 10.0.1
+ p-queue: 6.6.2
+ p-retry: 4.6.2
+ semver: 7.6.3
+ uuid: 10.0.0
+ optionalDependencies:
+ openai: 4.73.1(encoding@0.1.13)(zod@3.23.8)
+
language-subtag-registry@0.3.23: {}
language-tags@1.0.9:
@@ -14806,6 +15224,8 @@ snapshots:
multiformats@9.9.0: {}
+ mustache@4.2.0: {}
+
mz@2.7.0:
dependencies:
any-promise: 1.3.0
@@ -14880,6 +15300,8 @@ snapshots:
dependencies:
minimatch: 3.1.2
+ node-domexception@1.0.0: {}
+
node-fetch-native@1.6.4: {}
node-fetch@2.7.0(encoding@0.1.13):
@@ -15013,6 +15435,24 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
+ openai-edge@1.2.2: {}
+
+ openai@4.73.1(encoding@0.1.13)(zod@3.23.8):
+ dependencies:
+ '@types/node': 18.19.66
+ '@types/node-fetch': 2.6.12
+ abort-controller: 3.0.0
+ agentkeepalive: 4.5.0
+ form-data-encoder: 1.7.2
+ formdata-node: 4.4.1
+ node-fetch: 2.7.0(encoding@0.1.13)
+ optionalDependencies:
+ zod: 3.23.8
+ transitivePeerDependencies:
+ - encoding
+
+ openapi-types@12.1.3: {}
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -15038,6 +15478,8 @@ snapshots:
transitivePeerDependencies:
- zod
+ p-finally@1.0.0: {}
+
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
@@ -15058,6 +15500,20 @@ snapshots:
dependencies:
p-limit: 3.1.0
+ p-queue@6.6.2:
+ dependencies:
+ eventemitter3: 4.0.7
+ p-timeout: 3.2.0
+
+ p-retry@4.6.2:
+ dependencies:
+ '@types/retry': 0.12.0
+ retry: 0.13.1
+
+ p-timeout@3.2.0:
+ dependencies:
+ p-finally: 1.0.0
+
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {}
@@ -15461,6 +15917,11 @@ snapshots:
react: 18.2.0
scheduler: 0.23.2
+ react-error-boundary@3.1.4(react@18.2.0):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ react: 18.2.0
+
react-error-boundary@4.1.2(react@18.2.0):
dependencies:
'@babel/runtime': 7.26.0
@@ -16481,6 +16942,8 @@ snapshots:
uncrypto@0.1.3: {}
+ undici-types@5.26.5: {}
+
undici-types@6.19.8: {}
undici@5.28.4:
@@ -16620,6 +17083,8 @@ snapshots:
utils-merge@1.0.1: {}
+ uuid@10.0.0: {}
+
uuid@8.3.2: {}
uuid@9.0.1: {}
@@ -16799,6 +17264,8 @@ snapshots:
dependencies:
makeerror: 1.0.12
+ web-streams-polyfill@4.0.0-beta.3: {}
+
webauthn-p256@0.0.10:
dependencies:
'@noble/curves': 1.6.0
diff --git a/public/site.webmanifest b/public/site.webmanifest
index b8909b0..a66a4b0 100644
--- a/public/site.webmanifest
+++ b/public/site.webmanifest
@@ -1,19 +1,30 @@
{
- "name": "use",
- "short_name": "use",
+ "name": "Elron AI",
+ "short_name": "Elron",
+ "description": "AI chatbot that integrates with blockchain technologies",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#171717",
+ "theme_color": "#171717",
+ "orientation": "portrait",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "any maskable"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/favicon-180x180.webp",
+ "sizes": "180x180",
+ "type": "image/webp",
+ "purpose": "any"
}
- ],
- "theme_color": "#000000",
- "background_color": "#000000",
- "display": "standalone"
+ ]
}
diff --git a/src/__tests__/components/providers/settings-provider.test.tsx b/src/__tests__/components/providers/settings-provider.test.tsx
new file mode 100644
index 0000000..abe01ac
--- /dev/null
+++ b/src/__tests__/components/providers/settings-provider.test.tsx
@@ -0,0 +1,208 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import { renderHook, act } from '@testing-library/react-hooks'
+import { SettingsProvider, useSettings, defaultSettingsValue } from '@/components/providers/settings-provider'
+
+describe('SettingsProvider', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('useSettings Hook', () => {
+ it('returns default settings when no initial settings provided', () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useSettings(), { wrapper })
+ expect(result.current.settings).toEqual(defaultSettingsValue)
+ })
+
+ it('throws error when used outside provider', () => {
+ const { result } = renderHook(() => useSettings())
+ expect(result.error).toEqual(Error('useSettings must be used within a SettingsProvider'))
+ })
+
+ it('updates settings through hook', () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useSettings(), { wrapper })
+
+ act(() => {
+ result.current.updateSettings({ theme: 'dark' })
+ })
+
+ expect(result.current.settings.theme).toBe('dark')
+ })
+
+ it('merges partial updates correctly', () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useSettings(), { wrapper })
+ const initialTheme = result.current.settings.theme
+
+ act(() => {
+ result.current.updateSettings({ fontSize: 'large' })
+ })
+
+ expect(result.current.settings.theme).toBe(initialTheme)
+ expect(result.current.settings.fontSize).toBe('large')
+ })
+ })
+
+ describe('SettingsProvider Component', () => {
+ it('accepts and merges initial settings', () => {
+ const initialSettings = {
+ theme: 'dark' as const,
+ fontSize: 'large' as const,
+ }
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useSettings(), { wrapper })
+ expect(result.current.settings.theme).toBe(initialSettings.theme)
+ expect(result.current.settings.fontSize).toBe(initialSettings.fontSize)
+ expect(result.current.settings.notifications).toBe(defaultSettingsValue.notifications)
+ })
+
+ it('handles invalid settings gracefully', () => {
+ const invalidSettings = {
+ theme: 'invalid-theme' as any,
+ fontSize: 'invalid-size' as any,
+ }
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useSettings(), { wrapper })
+ expect(result.current.settings.theme).toBe(invalidSettings.theme)
+ expect(result.current.settings.fontSize).toBe(invalidSettings.fontSize)
+ })
+
+ it('updates multiple settings at once', () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useSettings(), { wrapper })
+
+ act(() => {
+ result.current.updateSettings({
+ theme: 'dark',
+ fontSize: 'small',
+ notifications: false,
+ })
+ })
+
+ expect(result.current.settings).toEqual({
+ theme: 'dark',
+ fontSize: 'small',
+ notifications: false,
+ })
+ })
+
+ it('preserves existing settings when updating with empty object', () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+
+ const { result } = renderHook(() => useSettings(), { wrapper })
+ const initialSettings = { ...result.current.settings }
+
+ act(() => {
+ result.current.updateSettings({})
+ })
+
+ expect(result.current.settings).toEqual(initialSettings)
+ })
+ })
+
+ describe('Integration Tests', () => {
+ it('updates settings across multiple components', () => {
+ const SettingsDisplay = () => {
+ const { settings } = useSettings()
+ return {JSON.stringify(settings)}
+ }
+
+ const SettingsUpdater = () => {
+ const { updateSettings } = useSettings()
+ return (
+ updateSettings({ theme: 'dark' })}
+ >
+ Update
+
+ )
+ }
+
+ render(
+
+
+
+
+ )
+
+ const display = screen.getByTestId('display')
+ const updater = screen.getByTestId('updater')
+
+ const initialSettings = JSON.parse(display.textContent!)
+ expect(initialSettings.theme).toBe('light')
+
+ fireEvent.click(updater)
+
+ const updatedSettings = JSON.parse(display.textContent!)
+ expect(updatedSettings.theme).toBe('dark')
+ })
+
+ it('maintains settings consistency across rerenders', () => {
+ const SettingsUser = () => {
+ const { settings, updateSettings } = useSettings()
+ const [count, setCount] = React.useState(0)
+
+ return (
+
+
{JSON.stringify(settings)}
+
setCount(c => c + 1)}>
+ Count: {count}
+
+
updateSettings({ theme: 'dark' })}
+ >
+ Update Settings
+
+
+ )
+ }
+
+ render(
+
+
+
+ )
+
+ // Initial state
+ const settingsDisplay = screen.getByTestId('settings')
+ const initialSettings = JSON.parse(settingsDisplay.textContent!)
+ expect(initialSettings.theme).toBe('light')
+
+ // Update settings
+ fireEvent.click(screen.getByTestId('update-settings'))
+ const updatedSettings = JSON.parse(settingsDisplay.textContent!)
+ expect(updatedSettings.theme).toBe('dark')
+
+ // Trigger rerender
+ fireEvent.click(screen.getByTestId('update-count'))
+ const settingsAfterRerender = JSON.parse(settingsDisplay.textContent!)
+ expect(settingsAfterRerender.theme).toBe('dark')
+ })
+ })
+})
\ No newline at end of file
diff --git a/src/components/providers/settings-provider.tsx b/src/components/providers/settings-provider.tsx
index e69de29..dab65ab 100644
--- a/src/components/providers/settings-provider.tsx
+++ b/src/components/providers/settings-provider.tsx
@@ -0,0 +1,55 @@
+import React, { createContext, useContext, useState } from 'react'
+
+interface Settings {
+ theme: 'light' | 'dark'
+ fontSize: 'small' | 'medium' | 'large'
+ notifications: boolean
+}
+
+interface SettingsContextType {
+ settings: Settings
+ updateSettings: (newSettings: Partial) => void
+}
+
+const defaultSettings: Settings = {
+ theme: 'light',
+ fontSize: 'medium',
+ notifications: true,
+}
+
+const SettingsContext = createContext(undefined)
+
+export function useSettings() {
+ const context = useContext(SettingsContext)
+ if (!context) {
+ throw new Error('useSettings must be used within a SettingsProvider')
+ }
+ return context
+}
+
+interface SettingsProviderProps {
+ children: React.ReactNode
+ initialSettings?: Partial
+}
+
+export function SettingsProvider({ children, initialSettings = {} }: SettingsProviderProps) {
+ const [settings, setSettings] = useState({
+ ...defaultSettings,
+ ...initialSettings,
+ })
+
+ const updateSettings = (newSettings: Partial) => {
+ setSettings((prev) => ({
+ ...prev,
+ ...newSettings,
+ }))
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const defaultSettingsValue = defaultSettings
diff --git a/supabase/config.toml b/supabase/config.toml
new file mode 100644
index 0000000..b25a8f3
--- /dev/null
+++ b/supabase/config.toml
@@ -0,0 +1,256 @@
+# A string used to distinguish different Supabase projects on the same host. Defaults to the
+# working directory name when running `supabase init`.
+project_id = "ai-bot-vercel"
+
+[api]
+enabled = true
+# Port to use for the API URL.
+port = 54321
+# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
+# endpoints. `public` is always included.
+schemas = ["public", "graphql_public"]
+# Extra schemas to add to the search_path of every request. `public` is always included.
+extra_search_path = ["public", "extensions"]
+# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
+# for accidental or malicious requests.
+max_rows = 1000
+
+[api.tls]
+enabled = false
+
+[db]
+# Port to use for the local database URL.
+port = 54322
+# Port used by db diff command to initialize the shadow database.
+shadow_port = 54320
+# The database major version to use. This has to be the same as your remote database's. Run `SHOW
+# server_version;` on the remote database to check.
+major_version = 15
+
+[db.pooler]
+enabled = false
+# Port to use for the local connection pooler.
+port = 54329
+# Specifies when a server connection can be reused by other clients.
+# Configure one of the supported pooler modes: `transaction`, `session`.
+pool_mode = "transaction"
+# How many server connections to allow per user/database pair.
+default_pool_size = 20
+# Maximum number of client connections allowed.
+max_client_conn = 100
+
+[db.seed]
+# If enabled, seeds the database after migrations during a db reset.
+enabled = true
+# Specifies an ordered list of seed files to load during db reset.
+# Supports glob patterns relative to supabase directory. For example:
+# sql_paths = ['./seeds/*.sql', '../project-src/seeds/*-load-testing.sql']
+sql_paths = ['./seed.sql']
+
+[realtime]
+enabled = true
+# Bind realtime via either IPv4 or IPv6. (default: IPv4)
+# ip_version = "IPv6"
+# The maximum length in bytes of HTTP request headers. (default: 4096)
+# max_header_length = 4096
+
+[studio]
+enabled = true
+# Port to use for Supabase Studio.
+port = 54323
+# External URL of the API server that frontend connects to.
+api_url = "http://127.0.0.1"
+# OpenAI API Key to use for Supabase AI in the Supabase Studio.
+openai_api_key = "env(OPENAI_API_KEY)"
+
+# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
+# are monitored, and you can view the emails that would have been sent from the web interface.
+[inbucket]
+enabled = true
+# Port to use for the email testing server web interface.
+port = 54324
+# Uncomment to expose additional ports for testing user applications that send emails.
+# smtp_port = 54325
+# pop3_port = 54326
+
+[storage]
+enabled = true
+# The maximum file size allowed (e.g. "5MB", "500KB").
+file_size_limit = "50MiB"
+
+[storage.image_transformation]
+enabled = true
+
+# Uncomment to configure local storage buckets
+# [storage.buckets.images]
+# public = false
+# file_size_limit = "50MiB"
+# allowed_mime_types = ["image/png", "image/jpeg"]
+# objects_path = "./images"
+
+[auth]
+enabled = true
+# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
+# in emails.
+site_url = "http://127.0.0.1:3000"
+# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
+additional_redirect_urls = ["https://127.0.0.1:3000"]
+# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
+jwt_expiry = 3600
+# If disabled, the refresh token will never expire.
+enable_refresh_token_rotation = true
+# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
+# Requires enable_refresh_token_rotation = true.
+refresh_token_reuse_interval = 10
+# Allow/disallow new user signups to your project.
+enable_signup = true
+# Allow/disallow anonymous sign-ins to your project.
+enable_anonymous_sign_ins = false
+# Allow/disallow testing manual linking of accounts
+enable_manual_linking = false
+
+[auth.email]
+# Allow/disallow new user signups via email to your project.
+enable_signup = true
+# If enabled, a user will be required to confirm any email change on both the old, and new email
+# addresses. If disabled, only the new email is required to confirm.
+double_confirm_changes = true
+# If enabled, users need to confirm their email address before signing in.
+enable_confirmations = false
+# If enabled, users will need to reauthenticate or have logged in recently to change their password.
+secure_password_change = false
+# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
+max_frequency = "1s"
+# Number of characters used in the email OTP.
+otp_length = 6
+# Number of seconds before the email OTP expires (defaults to 1 hour).
+otp_expiry = 3600
+
+# Use a production-ready SMTP server
+# [auth.email.smtp]
+# host = "smtp.sendgrid.net"
+# port = 587
+# user = "apikey"
+# pass = "env(SENDGRID_API_KEY)"
+# admin_email = "admin@email.com"
+# sender_name = "Admin"
+
+# Uncomment to customize email template
+# [auth.email.template.invite]
+# subject = "You have been invited"
+# content_path = "./supabase/templates/invite.html"
+
+[auth.sms]
+# Allow/disallow new user signups via SMS to your project.
+enable_signup = false
+# If enabled, users need to confirm their phone number before signing in.
+enable_confirmations = false
+# Template for sending OTP to users
+template = "Your code is {{ .Code }} ."
+# Controls the minimum amount of time that must pass before sending another sms otp.
+max_frequency = "5s"
+
+# Use pre-defined map of phone number to OTP for testing.
+# [auth.sms.test_otp]
+# 4152127777 = "123456"
+
+# Configure logged in session timeouts.
+# [auth.sessions]
+# Force log out after the specified duration.
+# timebox = "24h"
+# Force log out if the user has been inactive longer than the specified duration.
+# inactivity_timeout = "8h"
+
+# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
+# [auth.hook.custom_access_token]
+# enabled = true
+# uri = "pg-functions:////"
+
+# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
+[auth.sms.twilio]
+enabled = false
+account_sid = ""
+message_service_sid = ""
+# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
+auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
+
+[auth.mfa]
+# Control how many MFA factors can be enrolled at once per user.
+max_enrolled_factors = 10
+
+# Control use of MFA via App Authenticator (TOTP)
+[auth.mfa.totp]
+enroll_enabled = true
+verify_enabled = true
+
+# Configure Multi-factor-authentication via Phone Messaging
+# [auth.mfa.phone]
+# enroll_enabled = true
+# verify_enabled = true
+# otp_length = 6
+# template = "Your code is {{ .Code }} ."
+# max_frequency = "10s"
+
+# Configure Multi-factor-authentication via WebAuthn
+# [auth.mfa.web_authn]
+# enroll_enabled = true
+# verify_enabled = true
+
+# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
+# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
+# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
+[auth.external.apple]
+enabled = false
+client_id = ""
+# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
+secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
+# Overrides the default auth redirectUrl.
+redirect_uri = ""
+# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
+# or any other third-party OIDC providers.
+url = ""
+# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
+skip_nonce_check = false
+
+# Use Firebase Auth as a third-party provider alongside Supabase Auth.
+[auth.third_party.firebase]
+enabled = false
+# project_id = "my-firebase-project"
+
+# Use Auth0 as a third-party provider alongside Supabase Auth.
+[auth.third_party.auth0]
+enabled = false
+# tenant = "my-auth0-tenant"
+# tenant_region = "us"
+
+# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
+[auth.third_party.aws_cognito]
+enabled = false
+# user_pool_id = "my-user-pool-id"
+# user_pool_region = "us-east-1"
+
+[edge_runtime]
+enabled = true
+# Configure one of the supported request policies: `oneshot`, `per_worker`.
+# Use `oneshot` for hot reload, or `per_worker` for load testing.
+policy = "oneshot"
+inspector_port = 8083
+
+[analytics]
+enabled = true
+port = 54327
+# Configure one of the supported backends: `postgres`, `bigquery`.
+backend = "postgres"
+
+# Experimental features may be deprecated any time
+[experimental]
+# Configures Postgres storage engine to use OrioleDB (S3)
+orioledb_version = ""
+# Configures S3 bucket URL, eg. .s3-.amazonaws.com
+s3_host = "env(S3_HOST)"
+# Configures S3 bucket region, eg. us-east-1
+s3_region = "env(S3_REGION)"
+# Configures AWS_ACCESS_KEY_ID for S3 bucket
+s3_access_key = "env(S3_ACCESS_KEY)"
+# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
+s3_secret_key = "env(S3_SECRET_KEY)"
diff --git a/supabase/migrations/20240000000013_add_tasks.sql b/supabase/migrations/20240000000013_add_tasks.sql
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/supabase/migrations/20240000000013_add_tasks.sql
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/supabase/migrations/20241127063603_remote_schema.sql b/supabase/migrations/20241127063603_remote_schema.sql
new file mode 100644
index 0000000..518f691
--- /dev/null
+++ b/supabase/migrations/20241127063603_remote_schema.sql
@@ -0,0 +1,324 @@
+create type "public"."notification_type" as enum ('info', 'success', 'warning', 'error', 'crypto', 'wallet');
+
+create sequence "public"."notifications_id_seq";
+
+drop trigger if exists "tr_file_version" on "public"."file_uploads";
+
+drop policy "Users can delete their own files" on "public"."file_uploads";
+
+drop policy "Users can insert their own files" on "public"."file_uploads";
+
+drop policy "Users can view their own files" on "public"."file_uploads";
+
+drop policy "Users can delete their own tasks" on "public"."tasks";
+
+drop policy "Users can insert their own tasks" on "public"."tasks";
+
+drop policy "Users can update their own tasks" on "public"."tasks";
+
+drop policy "Users can view their own tasks" on "public"."tasks";
+
+revoke select on table "public"."file_uploads" from "PUBLIC";
+
+revoke delete on table "public"."tasks" from "anon";
+
+revoke insert on table "public"."tasks" from "anon";
+
+revoke references on table "public"."tasks" from "anon";
+
+revoke select on table "public"."tasks" from "anon";
+
+revoke trigger on table "public"."tasks" from "anon";
+
+revoke truncate on table "public"."tasks" from "anon";
+
+revoke update on table "public"."tasks" from "anon";
+
+revoke delete on table "public"."tasks" from "authenticated";
+
+revoke insert on table "public"."tasks" from "authenticated";
+
+revoke references on table "public"."tasks" from "authenticated";
+
+revoke select on table "public"."tasks" from "authenticated";
+
+revoke trigger on table "public"."tasks" from "authenticated";
+
+revoke truncate on table "public"."tasks" from "authenticated";
+
+revoke update on table "public"."tasks" from "authenticated";
+
+revoke delete on table "public"."tasks" from "service_role";
+
+revoke insert on table "public"."tasks" from "service_role";
+
+revoke references on table "public"."tasks" from "service_role";
+
+revoke select on table "public"."tasks" from "service_role";
+
+revoke trigger on table "public"."tasks" from "service_role";
+
+revoke truncate on table "public"."tasks" from "service_role";
+
+revoke update on table "public"."tasks" from "service_role";
+
+alter table "public"."file_uploads" drop constraint "file_uploads_unique_per_chat";
+
+alter table "public"."file_uploads" drop constraint "file_uploads_unique_version";
+
+alter table "public"."file_uploads" drop constraint "file_uploads_user_id_fkey";
+
+alter table "public"."tasks" drop constraint "progress_range";
+
+alter table "public"."tasks" drop constraint "tasks_status_check";
+
+alter table "public"."tasks" drop constraint "tasks_type_check";
+
+alter table "public"."tasks" drop constraint "tasks_user_id_fkey";
+
+alter table "public"."tasks" drop constraint "tasks_pkey";
+
+drop index if exists "public"."file_uploads_bucket_path_idx";
+
+drop index if exists "public"."file_uploads_chat_id_idx";
+
+drop index if exists "public"."file_uploads_created_at_idx";
+
+drop index if exists "public"."file_uploads_unique_per_chat";
+
+drop index if exists "public"."file_uploads_unique_version";
+
+drop index if exists "public"."file_uploads_user_id_idx";
+
+drop index if exists "public"."tasks_created_at_idx";
+
+drop index if exists "public"."tasks_pkey";
+
+drop index if exists "public"."tasks_status_idx";
+
+drop index if exists "public"."tasks_type_idx";
+
+drop index if exists "public"."tasks_user_id_idx";
+
+drop table "public"."tasks";
+
+create table "public"."notifications" (
+ "id" bigint not null default nextval('notifications_id_seq'::regclass),
+ "user_id" uuid not null,
+ "content" text not null,
+ "type" notification_type not null default 'info'::notification_type,
+ "is_read" boolean not null default false,
+ "created_at" timestamp with time zone not null default now()
+);
+
+
+alter table "public"."notifications" enable row level security;
+
+create table "public"."profiles" (
+ "id" uuid not null,
+ "email" text,
+ "full_name" text,
+ "avatar_url" text,
+ "provider" text,
+ "updated_at" timestamp with time zone not null default timezone('utc'::text, now()),
+ "created_at" timestamp with time zone not null default timezone('utc'::text, now())
+);
+
+
+alter table "public"."profiles" enable row level security;
+
+alter table "public"."file_uploads" alter column "id" drop default;
+
+alter table "public"."file_uploads" alter column "id" add generated always as identity;
+
+alter table "public"."file_uploads" alter column "id" set data type bigint using "id"::bigint;
+
+alter table "public"."file_uploads" disable row level security;
+
+alter table "public"."users" add column "last_sign_in" timestamp with time zone;
+
+alter table "public"."users" add column "nonce" text;
+
+alter table "public"."users" add column "wallet_address" text;
+
+alter sequence "public"."notifications_id_seq" owned by "public"."notifications"."id";
+
+CREATE INDEX idx_users_wallet_address ON public.users USING btree (wallet_address);
+
+CREATE UNIQUE INDEX notifications_pkey ON public.notifications USING btree (id);
+
+CREATE INDEX profiles_email_idx ON public.profiles USING btree (email);
+
+CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id);
+
+CREATE INDEX profiles_provider_idx ON public.profiles USING btree (provider);
+
+CREATE UNIQUE INDEX users_wallet_address_key ON public.users USING btree (wallet_address);
+
+alter table "public"."notifications" add constraint "notifications_pkey" PRIMARY KEY using index "notifications_pkey";
+
+alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey";
+
+alter table "public"."notifications" add constraint "notifications_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
+
+alter table "public"."notifications" validate constraint "notifications_user_id_fkey";
+
+alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
+
+alter table "public"."profiles" validate constraint "profiles_id_fkey";
+
+alter table "public"."profiles" add constraint "profiles_user_id_fkey" FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE not valid;
+
+alter table "public"."profiles" validate constraint "profiles_user_id_fkey";
+
+alter table "public"."users" add constraint "users_wallet_address_key" UNIQUE using index "users_wallet_address_key";
+
+grant delete on table "public"."notifications" to "anon";
+
+grant insert on table "public"."notifications" to "anon";
+
+grant references on table "public"."notifications" to "anon";
+
+grant select on table "public"."notifications" to "anon";
+
+grant trigger on table "public"."notifications" to "anon";
+
+grant truncate on table "public"."notifications" to "anon";
+
+grant update on table "public"."notifications" to "anon";
+
+grant delete on table "public"."notifications" to "authenticated";
+
+grant insert on table "public"."notifications" to "authenticated";
+
+grant references on table "public"."notifications" to "authenticated";
+
+grant select on table "public"."notifications" to "authenticated";
+
+grant trigger on table "public"."notifications" to "authenticated";
+
+grant truncate on table "public"."notifications" to "authenticated";
+
+grant update on table "public"."notifications" to "authenticated";
+
+grant delete on table "public"."notifications" to "service_role";
+
+grant insert on table "public"."notifications" to "service_role";
+
+grant references on table "public"."notifications" to "service_role";
+
+grant select on table "public"."notifications" to "service_role";
+
+grant trigger on table "public"."notifications" to "service_role";
+
+grant truncate on table "public"."notifications" to "service_role";
+
+grant update on table "public"."notifications" to "service_role";
+
+grant delete on table "public"."profiles" to "anon";
+
+grant insert on table "public"."profiles" to "anon";
+
+grant references on table "public"."profiles" to "anon";
+
+grant select on table "public"."profiles" to "anon";
+
+grant trigger on table "public"."profiles" to "anon";
+
+grant truncate on table "public"."profiles" to "anon";
+
+grant update on table "public"."profiles" to "anon";
+
+grant delete on table "public"."profiles" to "authenticated";
+
+grant insert on table "public"."profiles" to "authenticated";
+
+grant references on table "public"."profiles" to "authenticated";
+
+grant select on table "public"."profiles" to "authenticated";
+
+grant trigger on table "public"."profiles" to "authenticated";
+
+grant truncate on table "public"."profiles" to "authenticated";
+
+grant update on table "public"."profiles" to "authenticated";
+
+grant delete on table "public"."profiles" to "service_role";
+
+grant insert on table "public"."profiles" to "service_role";
+
+grant references on table "public"."profiles" to "service_role";
+
+grant select on table "public"."profiles" to "service_role";
+
+grant trigger on table "public"."profiles" to "service_role";
+
+grant truncate on table "public"."profiles" to "service_role";
+
+grant update on table "public"."profiles" to "service_role";
+
+create policy "Users can insert their own notifications"
+on "public"."notifications"
+as permissive
+for insert
+to public
+with check ((auth.uid() = user_id));
+
+
+create policy "Users can update their own notifications"
+on "public"."notifications"
+as permissive
+for update
+to public
+using ((auth.uid() = user_id));
+
+
+create policy "Users can view their own notifications"
+on "public"."notifications"
+as permissive
+for select
+to public
+using ((auth.uid() = user_id));
+
+
+create policy "Create profile on signup"
+on "public"."profiles"
+as permissive
+for insert
+to public
+with check ((auth.uid() = id));
+
+
+create policy "Users can update own profile"
+on "public"."profiles"
+as permissive
+for update
+to public
+using ((auth.uid() = id));
+
+
+create policy "Users can view own profile"
+on "public"."profiles"
+as permissive
+for select
+to public
+using ((auth.uid() = id));
+
+
+create policy "Users can update own data"
+on "public"."users"
+as permissive
+for update
+to public
+using (((auth.uid() = id) OR (wallet_address = ((current_setting('request.jwt.claims'::text))::json ->> 'wallet_address'::text))));
+
+
+create policy "Users can view own data"
+on "public"."users"
+as permissive
+for select
+to public
+using (((auth.uid() = id) OR (wallet_address = ((current_setting('request.jwt.claims'::text))::json ->> 'wallet_address'::text))));
+
+
+
diff --git a/tsconfig.json b/tsconfig.json
index 2eb129b..ed65dc3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "ESNext",
+ "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -19,41 +19,17 @@
}
],
"paths": {
- "@/*": ["./*"],
- "@/app/*": ["./app/*"],
- "@/components/*": ["./components/*"],
- "@/lib/*": ["./lib/*"],
- "@/hooks/*": ["./hooks/*"],
- "@/utils/*": ["./utils/*"],
- "@/tests/*": ["./__tests__/*"],
- "@/components/custom/*": ["./components/custom/*"],
- "@/ai/*": ["./ai/*"],
- "@/db/*": ["./db/*"],
- "@/actions/*": ["./actions/*"],
- "@/public/*": ["./public/*"]
+ "@/*": ["./*"]
},
- "types": [
- "node",
- "@types/react",
- "@types/react-dom",
- "@testing-library/jest-dom",
- "vitest/importMeta",
- "vitest/globals"
- ]
+ "types": ["@testing-library/jest-dom"]
},
"include": [
+ "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
- ".",
- "__tests__/**/*",
".next/types/**/*.ts"
],
"exclude": [
- "node_modules",
- ".next",
- "coverage",
- "dist",
- "__test__",
- "**/__tests__/**"
+ "node_modules"
]
}