Compare commits

...

2 Commits

Author SHA1 Message Date
db2e7d8017 feat: Add Save/Save Draft button to chat interface
- Add dynamic button that shows "Save" when no draft exists
- Changes to "Save Draft" when a pending draft is in progress
- Uses floppy disk icon (IconDeviceFloppy) for visual consistency
- Clicking "Save" creates empty draft and navigates to edit page
- Clicking "Save Draft" navigates to edit page with existing draft
- Reactive state tracking using useSelector for draft state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 15:57:26 +00:00
45dfbc42d3 feat: Add production schema deployment scripts
- Create apply-schema-prod.js for deploying schema to any SurrealDB instance
- Create deploy-schema.sh to easily deploy using .prod.env
- Add npm scripts: schema:apply (local) and schema:deploy (production)

Usage:
  pnpm schema:deploy  # Deploy to production using .prod.env

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 15:44:05 +00:00
4 changed files with 174 additions and 2 deletions

View File

@@ -15,12 +15,13 @@ import {
Tooltip,
} from '@mantine/core';
import { useRef, useEffect, useState } from 'react';
import { IconVolume, IconMicrophone, IconNotes } from '@tabler/icons-react';
import { IconVolume, IconMicrophone, IconDeviceFloppy } from '@tabler/icons-react';
import { UserMenu } from '@/components/UserMenu';
import { useVoiceMode } from '@/hooks/useVoiceMode';
import { useAppMachine } from '@/hooks/useAppMachine';
import { notifications } from '@mantine/notifications';
import { useMediaQuery } from '@mantine/hooks';
import { useSelector } from '@xstate/react';
/**
* Get the voice button text based on the current state
@@ -62,6 +63,10 @@ export default function ChatPage() {
// State for creating node
const [isCreatingNode, setIsCreatingNode] = useState(false);
// Check if we have a pending draft (using useSelector for reactivity)
const pendingNodeDraft = useSelector(appActor, (state) => state.context.pendingNodeDraft);
const hasPendingDraft = !!pendingNodeDraft;
// Use the clean voice mode hook
const { state, send, transcript, error } = useVoiceMode({
messages,
@@ -122,6 +127,34 @@ export default function ChatPage() {
}
};
// Handler for Manual/Save button
const handleManualOrSave = () => {
if (hasPendingDraft && pendingNodeDraft) {
// If we have a draft, navigate to edit with it
appActor.send({
type: 'NAVIGATE_TO_EDIT',
draft: pendingNodeDraft,
});
} else {
// Create an empty draft for manual entry
const emptyDraft = {
title: '',
content: '',
conversationContext: messages.map((m) => {
if ('parts' in m && Array.isArray((m as any).parts)) {
return `${m.role}: ${(m as any).parts.find((p: any) => p.type === 'text')?.text || ''}`;
}
return `${m.role}: ${(m as any).content || ''}`;
}).join('\n'),
};
appActor.send({
type: 'CREATE_NODE_FROM_CONVERSATION',
draft: emptyDraft,
});
}
};
// Add initial greeting message on first load
useEffect(() => {
if (messages.length === 0) {
@@ -377,6 +410,15 @@ export default function ChatPage() {
variant="filled"
disabled={isVoiceActive}
/>
<Button
onClick={handleManualOrSave}
radius="xl"
variant={hasPendingDraft ? 'filled' : 'light'}
color={hasPendingDraft ? 'blue' : 'gray'}
leftSection={<IconDeviceFloppy size={20} />}
>
{hasPendingDraft ? 'Save Draft' : 'Save'}
</Button>
<Button
type="submit"
radius="xl"

View File

@@ -7,7 +7,9 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "npx magnitude"
"test": "npx magnitude",
"schema:apply": "node scripts/apply-schema.js",
"schema:deploy": "./scripts/deploy-schema.sh"
},
"dependencies": {
"@ai-sdk/google": "latest",

105
scripts/apply-schema-prod.js Executable file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* Apply SurrealDB schema to production database
*
* Usage:
* SURREALDB_URL=<url> SURREALDB_USER=<user> SURREALDB_PASS=<pass> \
* SURREALDB_NS=<ns> SURREALDB_DB=<db> SURREALDB_JWT_SECRET=<secret> \
* node scripts/apply-schema-prod.js
*/
const Surreal = require('surrealdb').default;
const fs = require('fs');
const path = require('path');
async function applySchema() {
// Read config from environment
const config = {
url: process.env.SURREALDB_URL,
username: process.env.SURREALDB_USER,
password: process.env.SURREALDB_PASS,
namespace: process.env.SURREALDB_NS,
database: process.env.SURREALDB_DB,
jwtSecret: process.env.SURREALDB_JWT_SECRET,
};
// Validate required config
const missing = Object.entries(config)
.filter(([_, value]) => !value)
.map(([key]) => key);
if (missing.length > 0) {
console.error('[Schema] ✗ Missing required environment variables:', missing.join(', '));
console.error('[Schema] Please set all required variables:');
console.error('[Schema] SURREALDB_URL, SURREALDB_USER, SURREALDB_PASS,');
console.error('[Schema] SURREALDB_NS, SURREALDB_DB, SURREALDB_JWT_SECRET');
process.exit(1);
}
const db = new Surreal();
try {
console.log('[Schema] Connecting to SurrealDB...');
console.log(`[Schema] URL: ${config.url}`);
await db.connect(config.url);
console.log('[Schema] Signing in...');
await db.signin({
username: config.username,
password: config.password,
});
console.log('[Schema] Using namespace and database...');
console.log(`[Schema] Namespace: ${config.namespace}`);
console.log(`[Schema] Database: ${config.database}`);
await db.use({
namespace: config.namespace,
database: config.database,
});
console.log('[Schema] Reading schema file...');
const schemaPath = path.join(__dirname, '..', 'db', 'schema.surql');
let schema = fs.readFileSync(schemaPath, 'utf-8');
// Replace $env.SURREALDB_JWT_SECRET with actual value
schema = schema.replace('$env.SURREALDB_JWT_SECRET', `'${config.jwtSecret}'`);
console.log('[Schema] Executing schema...');
let result;
try {
result = await db.query(schema);
} catch (error) {
// If error contains "already exists", it's OK - schema was already applied
if (error.message.includes('already exists')) {
console.log('[Schema] ⚠ Some schema elements already exist (this is OK)');
console.log('[Schema] ✓ Schema is up to date!');
return;
} else {
throw error;
}
}
if (result) {
console.log(`[Schema] Executed ${result.length} queries`);
// Log any errors
result.forEach((r, i) => {
if (r.status === 'ERR') {
console.error(`[Schema] Error in query ${i + 1}:`, r.result);
} else {
console.log(`[Schema] ✓ Query ${i + 1} succeeded`);
}
});
}
console.log('[Schema] ✓ Schema applied successfully!');
} catch (error) {
console.error('[Schema] ✗ Failed to apply schema:', error);
process.exit(1);
} finally {
await db.close();
}
}
applySchema();

23
scripts/deploy-schema.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Deploy schema to production SurrealDB
# This script reads from .prod.env and applies the schema
set -e
if [ ! -f .prod.env ]; then
echo "Error: .prod.env file not found"
echo "Please create .prod.env with your production database credentials"
exit 1
fi
echo "Loading production environment variables..."
export $(cat .prod.env | grep -v '^#' | grep '=' | xargs)
echo "Applying schema to production database..."
node scripts/apply-schema-prod.js
echo ""
echo "✓ Schema deployment complete!"
echo ""
echo "You can now test your deployment at: https://www.ponderants.com"