feat: add use cases marketing product with AI-generated content
Transform qualifying scenarios into marketing-ready use cases with: - AI-generated titles, descriptions, ROI estimates, business value - Persona/industry/category taxonomy for targeting - Browseable feed with filtering and ranking - SEO-optimized case study pages - Daily cron job for generation and ranking Database: - Add Persona, Industry, Category lookup tables - Add UseCase model with marketing content fields - Add junction tables for personas/industries/categories - Add SocialProof model for cached metrics API: - GET /api/use-cases - Global directory with filtering - GET /api/use-cases/[id] - Individual use case details - GET /api/public/users/[username]/collections/[slug]/use-cases - POST /api/cron/use-cases - Nightly generation job Frontend: - /use-cases - Global feed with persona dropdown - /use-cases/[slug] - SEO case study page - /[username]/collections/[slug]/use-cases - Collection feed - UseCasesFeed component - Sortable table component - UseCaseCaseStudy component - Full case study layout
This commit is contained in:
parent
5fafc90e3e
commit
48c41e8733
245 changed files with 4734 additions and 2075 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import * as readline from 'node:readline';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../../lib/api-client.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
import { getApiKey, getApiUrl } from '../../lib/config.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
export default class AgentChat extends Command {
|
||||
static description = 'Chat with an agent';
|
||||
|
|
@ -202,11 +202,11 @@ export default class AgentChat extends Command {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({})) as { message?: string };
|
||||
const errorData = (await response.json().catch(() => ({}))) as { message?: string };
|
||||
throw new Error(errorData.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as {
|
||||
const data = (await response.json()) as {
|
||||
content: string;
|
||||
conversationId: string;
|
||||
toolCalls?: { name: string; result: unknown }[];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as readline from 'node:readline';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../../lib/api-client.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
import * as readline from 'node:readline';
|
||||
|
||||
export default class AgentDelete extends Command {
|
||||
static description = 'Delete an agent';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import { hasCredentials, getApiKey, getApiUrl } from '../../lib/config.js';
|
||||
import { TpmClient } from '../../lib/api-client.js';
|
||||
import { getApiKey, getApiUrl, hasCredentials } from '../../lib/config.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
export default class Status extends Command {
|
||||
|
|
@ -80,7 +80,7 @@ export default class Status extends Command {
|
|||
authSource,
|
||||
apiUrl,
|
||||
user: response.data,
|
||||
keyPrefix: apiKey.substring(0, 12) + '...',
|
||||
keyPrefix: `${apiKey.substring(0, 12)}...`,
|
||||
});
|
||||
} else {
|
||||
output.newLine();
|
||||
|
|
@ -92,8 +92,11 @@ export default class Status extends Command {
|
|||
output.keyValue('Name', response.data.name);
|
||||
}
|
||||
output.keyValue('API URL', apiUrl);
|
||||
output.keyValue('Auth Source', authSource === 'env' ? 'Environment variable' : 'Config file');
|
||||
output.keyValue('Key Prefix', apiKey.substring(0, 12) + '...');
|
||||
output.keyValue(
|
||||
'Auth Source',
|
||||
authSource === 'env' ? 'Environment variable' : 'Config file'
|
||||
);
|
||||
output.keyValue('Key Prefix', `${apiKey.substring(0, 12)}...`);
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to verify authentication');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import * as readline from 'node:readline';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../../lib/api-client.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import * as fs from 'node:fs';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../../lib/api-client.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import { createOutput } from '../../lib/output.js';
|
|||
export default class CollectionRemove extends Command {
|
||||
static description = 'Remove a tool from a collection';
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> <%= command.id %> my-collection tool-id-1',
|
||||
];
|
||||
static examples = ['<%= config.bin %> <%= command.id %> my-collection tool-id-1'];
|
||||
|
||||
static flags = {
|
||||
json: Flags.boolean({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import { getApiKey, getApiUrl, getConfigDir, hasCredentials, getConfig } from '../lib/config.js';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import { TpmClient } from '../lib/api-client.js';
|
||||
import { getApiKey, getApiUrl, getConfig, getConfigDir, hasCredentials } from '../lib/config.js';
|
||||
import { createOutput } from '../lib/output.js';
|
||||
|
||||
interface DiagnosticCheck {
|
||||
|
|
@ -59,7 +59,11 @@ export default class Doctor extends Command {
|
|||
|
||||
// 3. Check authentication
|
||||
const hasAuth = hasCredentials() || !!process.env.TPMJS_API_KEY;
|
||||
const authSource = process.env.TPMJS_API_KEY ? 'environment' : hasCredentials() ? 'config file' : 'none';
|
||||
const authSource = process.env.TPMJS_API_KEY
|
||||
? 'environment'
|
||||
: hasCredentials()
|
||||
? 'config file'
|
||||
: 'none';
|
||||
checks.push({
|
||||
name: 'Authentication',
|
||||
status: hasAuth ? 'ok' : 'warning',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import * as fs from 'node:fs';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
type ClientType = 'claude' | 'cursor' | 'windsurf' | 'generic';
|
||||
|
|
@ -75,9 +75,10 @@ export default class McpConfig extends Command {
|
|||
}
|
||||
|
||||
// Merge mcpServers
|
||||
const existingServers = typeof existingConfig.mcpServers === 'object' && existingConfig.mcpServers !== null
|
||||
? existingConfig.mcpServers as Record<string, unknown>
|
||||
: {};
|
||||
const existingServers =
|
||||
typeof existingConfig.mcpServers === 'object' && existingConfig.mcpServers !== null
|
||||
? (existingConfig.mcpServers as Record<string, unknown>)
|
||||
: {};
|
||||
const mergedConfig = {
|
||||
...existingConfig,
|
||||
mcpServers: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import * as http from 'node:http';
|
||||
import * as readline from 'node:readline';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../../lib/api-client.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import * as readline from 'node:readline';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../lib/api-client.js';
|
||||
import { createOutput } from '../lib/output.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../../lib/api-client.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
|
|
@ -67,9 +67,7 @@ export default class PublishCheck extends Command {
|
|||
}
|
||||
|
||||
// Find exact match
|
||||
const exactMatch = searchResponse.data?.find(
|
||||
(tool) => tool.npmPackageName === packageName
|
||||
);
|
||||
const exactMatch = searchResponse.data?.find((tool) => tool.npmPackageName === packageName);
|
||||
|
||||
if (flags.json) {
|
||||
output.json({
|
||||
|
|
@ -88,7 +86,9 @@ export default class PublishCheck extends Command {
|
|||
output.text(`Category: ${exactMatch.category}`);
|
||||
output.text(`Tier: ${exactMatch.tier}`);
|
||||
output.text(`Version: ${exactMatch.npmVersion}`);
|
||||
output.text(`Downloads (last month): ${exactMatch.npmDownloadsLastMonth?.toLocaleString() || 'N/A'}`);
|
||||
output.text(
|
||||
`Downloads (last month): ${exactMatch.npmDownloadsLastMonth?.toLocaleString() || 'N/A'}`
|
||||
);
|
||||
output.text(`Quality Score: ${exactMatch.qualityScore?.toFixed(2) || 'N/A'}`);
|
||||
output.text('');
|
||||
output.text(`View on tpmjs.com:`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
export default class PublishPreview extends Command {
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export default class ScenarioList extends Command {
|
|||
if (scenarios.length === 0) {
|
||||
output.info(`No scenarios found for collection "${collection.name}"`);
|
||||
output.text(
|
||||
'Generate some with: tpm scenario generate ' + (collection.slug || collection.id)
|
||||
`Generate some with: tpm scenario generate ${collection.slug || collection.id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -112,8 +112,8 @@ export default class ScenarioList extends Command {
|
|||
output.text(output.bold(`Scenarios for ${collection.name}\n`));
|
||||
output.table(
|
||||
scenarios.map((s) => ({
|
||||
name: s.name || s.prompt.slice(0, 30) + '...',
|
||||
quality: (s.qualityScore * 100).toFixed(0) + '%',
|
||||
name: s.name || `${s.prompt.slice(0, 30)}...`,
|
||||
quality: `${(s.qualityScore * 100).toFixed(0)}%`,
|
||||
runs: s.totalRuns,
|
||||
status: s.lastRunStatus || '-',
|
||||
tags: s.tags.slice(0, 3).join(', ') || '-',
|
||||
|
|
@ -147,9 +147,9 @@ export default class ScenarioList extends Command {
|
|||
|
||||
output.table(
|
||||
response.data.map((s) => ({
|
||||
name: s.name || s.prompt.slice(0, 30) + '...',
|
||||
name: s.name || `${s.prompt.slice(0, 30)}...`,
|
||||
collection: s.collection?.name || '-',
|
||||
quality: (s.qualityScore * 100).toFixed(0) + '%',
|
||||
quality: `${(s.qualityScore * 100).toFixed(0)}%`,
|
||||
runs: s.totalRuns,
|
||||
status: s.lastRunStatus || '-',
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export default class ScenarioRun extends Command {
|
|||
let errors = 0;
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const name = scenario.name || scenario.prompt.slice(0, 40) + '...';
|
||||
const name = scenario.name || `${scenario.prompt.slice(0, 40)}...`;
|
||||
const runSpinner = output.spinner(`Running: ${name}`);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default class ScenarioTest extends Command {
|
|||
}
|
||||
).data;
|
||||
|
||||
scenarioName = scenario.name || scenario.prompt.slice(0, 50) + '...';
|
||||
scenarioName = scenario.name || `${scenario.prompt.slice(0, 50)}...`;
|
||||
infoSpinner.stop();
|
||||
|
||||
output.text(output.bold(`Scenario: ${scenarioName}`));
|
||||
|
|
@ -131,7 +131,7 @@ export default class ScenarioTest extends Command {
|
|||
} else {
|
||||
const truncatedReason =
|
||||
runData.evaluator.reason.length > 80
|
||||
? runData.evaluator.reason.slice(0, 80) + '...'
|
||||
? `${runData.evaluator.reason.slice(0, 80)}...`
|
||||
: runData.evaluator.reason;
|
||||
output.text(` Reason: ${truncatedReason}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ export default class ToolExecute extends Command {
|
|||
const content = fs.readFileSync(flags['input-file'], 'utf-8');
|
||||
params = JSON.parse(content);
|
||||
} catch (error) {
|
||||
output.error(`Failed to read input file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
output.error(
|
||||
`Failed to read input file: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default class ToolInfo extends Command {
|
|||
|
||||
output.keyValue('Package', tool.package?.npmPackageName || tool.npmPackageName);
|
||||
output.keyValue('Category', tool.package?.category || tool.category);
|
||||
output.keyValue('Official', (tool.package?.isOfficial || tool.isOfficial) ? 'Yes' : 'No');
|
||||
output.keyValue('Official', tool.package?.isOfficial || tool.isOfficial ? 'Yes' : 'No');
|
||||
output.newLine();
|
||||
|
||||
output.subheading('Description');
|
||||
|
|
@ -75,13 +75,20 @@ export default class ToolInfo extends Command {
|
|||
|
||||
output.subheading('Metrics');
|
||||
output.keyValue('Quality Score', tool.qualityScore ? tool.qualityScore.toFixed(2) : 'N/A');
|
||||
output.keyValue('Downloads/Month', formatDownloads(tool.package?.npmDownloadsLastMonth || tool.npmDownloadsLastMonth));
|
||||
output.keyValue(
|
||||
'Downloads/Month',
|
||||
formatDownloads(tool.package?.npmDownloadsLastMonth || tool.npmDownloadsLastMonth)
|
||||
);
|
||||
output.keyValue('Likes', tool.likeCount.toString());
|
||||
output.newLine();
|
||||
|
||||
output.subheading('Links');
|
||||
output.text(`Web: ${output.link('View on TPMJS', `https://tpmjs.com/tool/${args.package}/${args.tool}`)}`);
|
||||
output.text(`npm: ${output.link('View on npm', `https://www.npmjs.com/package/${args.package}`)}`);
|
||||
output.text(
|
||||
`Web: ${output.link('View on TPMJS', `https://tpmjs.com/tool/${args.package}/${args.tool}`)}`
|
||||
);
|
||||
output.text(
|
||||
`npm: ${output.link('View on npm', `https://www.npmjs.com/package/${args.package}`)}`
|
||||
);
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to fetch tool info');
|
||||
output.error(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as readline from 'node:readline';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
const CATEGORIES = [
|
||||
|
|
@ -154,7 +154,8 @@ export default class ToolInit extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
const author = (await this.prompt(`Author (${process.env.USER}): `)) || process.env.USER || 'unknown';
|
||||
const author =
|
||||
(await this.prompt(`Author (${process.env.USER}): `)) || process.env.USER || 'unknown';
|
||||
|
||||
return { name: toolName, description, category: category ?? 'utilities', author };
|
||||
}
|
||||
|
|
@ -219,7 +220,7 @@ export default class ToolInit extends Command {
|
|||
],
|
||||
...(template === 'rich'
|
||||
? {
|
||||
documentation: 'https://github.com/yourname/' + config.name + '#readme',
|
||||
documentation: `https://github.com/yourname/${config.name}#readme`,
|
||||
examples: [
|
||||
{
|
||||
title: 'Basic usage',
|
||||
|
|
@ -231,10 +232,7 @@ export default class ToolInit extends Command {
|
|||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(targetDir, 'package.json'),
|
||||
JSON.stringify(packageJson, null, 2)
|
||||
);
|
||||
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// tsconfig.json
|
||||
const tsconfig = {
|
||||
|
|
@ -254,10 +252,7 @@ export default class ToolInit extends Command {
|
|||
exclude: ['node_modules', 'dist'],
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(targetDir, 'tsconfig.json'),
|
||||
JSON.stringify(tsconfig, null, 2)
|
||||
);
|
||||
fs.writeFileSync(path.join(targetDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
||||
|
||||
// tsup.config.ts
|
||||
const tsupConfig = `import { defineConfig } from 'tsup';
|
||||
|
|
|
|||
|
|
@ -102,11 +102,7 @@ export default class ToolSearch extends Command {
|
|||
);
|
||||
|
||||
if (response.pagination.hasMore) {
|
||||
output.text(
|
||||
output.dim(
|
||||
`Use --offset ${flags.offset + flags.limit} to see more results`
|
||||
)
|
||||
);
|
||||
output.text(output.dim(`Use --offset ${flags.offset + flags.limit} to see more results`));
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail('Search failed');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import { getClient } from '../../lib/api-client.js';
|
||||
import { createOutput } from '../../lib/output.js';
|
||||
|
||||
|
|
@ -47,7 +47,10 @@ export default class ToolValidate extends Command {
|
|||
const content = fs.readFileSync(packagePath, 'utf-8');
|
||||
packageJson = JSON.parse(content);
|
||||
} catch (error) {
|
||||
output.error('Failed to parse package.json', error instanceof Error ? error.message : undefined);
|
||||
output.error(
|
||||
'Failed to parse package.json',
|
||||
error instanceof Error ? error.message : undefined
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +107,10 @@ export default class ToolValidate extends Command {
|
|||
output.success('Configuration is valid');
|
||||
output.newLine();
|
||||
output.keyValue('Tier', response.data.tier || 'minimal');
|
||||
output.keyValue('Has tpmjs keyword', hasTpmjsKeyword ? 'Yes' : 'No (add for auto-discovery)');
|
||||
output.keyValue(
|
||||
'Has tpmjs keyword',
|
||||
hasTpmjsKeyword ? 'Yes' : 'No (add for auto-discovery)'
|
||||
);
|
||||
|
||||
if (!hasTpmjsKeyword) {
|
||||
output.newLine();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Command, Flags } from '@oclif/core';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import { createOutput } from '../lib/output.js';
|
||||
|
||||
export default class Update extends Command {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Hook } from '@oclif/core';
|
||||
|
||||
const hook: Hook<'init'> = async function () {
|
||||
const hook: Hook<'init'> = async () => {
|
||||
// Initialization hook - runs before any command
|
||||
// Can be used for:
|
||||
// - Checking for updates
|
||||
|
|
|
|||
|
|
@ -1,36 +1,35 @@
|
|||
// Library exports for programmatic use
|
||||
export { TpmClient, getClient, ApiError } from './lib/api-client.js';
|
||||
|
||||
export type {
|
||||
TpmClientOptions,
|
||||
Agent,
|
||||
ApiKey,
|
||||
ApiResponse,
|
||||
PaginationOptions,
|
||||
Collection,
|
||||
CreateAgentInput,
|
||||
CreateCollectionInput,
|
||||
PaginatedResponse,
|
||||
PaginationOptions,
|
||||
Stats,
|
||||
Tool,
|
||||
ToolSearchOptions,
|
||||
Agent,
|
||||
CreateAgentInput,
|
||||
TpmClientOptions,
|
||||
UpdateAgentInput,
|
||||
Collection,
|
||||
CreateCollectionInput,
|
||||
UpdateCollectionInput,
|
||||
User,
|
||||
ApiKey,
|
||||
Stats,
|
||||
} from './lib/api-client.js';
|
||||
|
||||
export { ApiError, getClient, TpmClient } from './lib/api-client.js';
|
||||
export type { TpmConfig, TpmCredentials } from './lib/config.js';
|
||||
export {
|
||||
getConfig,
|
||||
setConfig,
|
||||
getConfigValue,
|
||||
setConfigValue,
|
||||
loadCredentials,
|
||||
saveCredentials,
|
||||
deleteCredentials,
|
||||
hasCredentials,
|
||||
getApiKey,
|
||||
getApiUrl,
|
||||
getConfig,
|
||||
getConfigValue,
|
||||
hasCredentials,
|
||||
loadCredentials,
|
||||
saveCredentials,
|
||||
setConfig,
|
||||
setConfigValue,
|
||||
} from './lib/config.js';
|
||||
export type { TpmConfig, TpmCredentials } from './lib/config.js';
|
||||
|
||||
export { OutputFormatter, createOutput } from './lib/output.js';
|
||||
export type { OutputOptions } from './lib/output.js';
|
||||
export { createOutput, OutputFormatter } from './lib/output.js';
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ export class TpmClient {
|
|||
};
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
|
@ -357,7 +357,7 @@ export class TpmClient {
|
|||
};
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Conf from 'conf';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import Conf from 'conf';
|
||||
|
||||
export interface TpmConfig {
|
||||
apiUrl?: string;
|
||||
|
|
@ -57,10 +57,7 @@ export function getConfigValue<K extends keyof TpmConfig>(key: K): TpmConfig[K]
|
|||
return configStore.get(key);
|
||||
}
|
||||
|
||||
export function setConfigValue<K extends keyof TpmConfig>(
|
||||
key: K,
|
||||
value: TpmConfig[K]
|
||||
): void {
|
||||
export function setConfigValue<K extends keyof TpmConfig>(key: K, value: TpmConfig[K]): void {
|
||||
configStore.set(key, value);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,13 +107,13 @@ export class OutputFormatter {
|
|||
// Key-value pair
|
||||
keyValue(key: string, value: string | number | boolean | undefined): void {
|
||||
if (this.options.json) return;
|
||||
console.log(`${pc.dim(key + ':')} ${value ?? pc.dim('(not set)')}`);
|
||||
console.log(`${pc.dim(`${key}:`)} ${value ?? pc.dim('(not set)')}`);
|
||||
}
|
||||
|
||||
// List item
|
||||
listItem(text: string, indent = 0): void {
|
||||
if (this.options.json) return;
|
||||
const prefix = ' '.repeat(indent) + '•';
|
||||
const prefix = `${' '.repeat(indent)}•`;
|
||||
console.log(`${prefix} ${text}`);
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ export class OutputFormatter {
|
|||
this.json({ code: text, language });
|
||||
return;
|
||||
}
|
||||
console.log(pc.dim('```' + (language ?? '')));
|
||||
console.log(pc.dim(`\`\`\`${language ?? ''}`));
|
||||
console.log(text);
|
||||
console.log(pc.dim('```'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'src/index.ts',
|
||||
'src/commands/**/*.ts',
|
||||
'src/hooks/**/*.ts',
|
||||
],
|
||||
entry: ['src/index.ts', 'src/commands/**/*.ts', 'src/hooks/**/*.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
|
|
|
|||
|
|
@ -1158,6 +1158,7 @@ model Scenario {
|
|||
// Relations
|
||||
runs ScenarioRun[]
|
||||
embedding ScenarioEmbedding?
|
||||
useCase UseCase?
|
||||
|
||||
@@index([collectionId])
|
||||
@@index([qualityScore])
|
||||
|
|
@ -1252,3 +1253,164 @@ model ScenarioQuota {
|
|||
@@index([userId])
|
||||
@@map("scenario_quotas")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Use Cases Models (Marketing Content from Scenarios)
|
||||
// ============================================================================
|
||||
|
||||
/// Persona - user personas for use case targeting (AI-populated)
|
||||
model Persona {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(100)
|
||||
slug String @unique @db.VarChar(100)
|
||||
description String? @db.Text
|
||||
icon String? @db.VarChar(50) // emoji or icon name
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
// Relations
|
||||
useCases UseCasePersona[]
|
||||
|
||||
@@index([slug])
|
||||
@@map("personas")
|
||||
}
|
||||
|
||||
/// Industry - industries for use case categorization (AI-populated)
|
||||
model Industry {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(100)
|
||||
slug String @unique @db.VarChar(100)
|
||||
description String? @db.Text
|
||||
icon String? @db.VarChar(50)
|
||||
parentId String? @map("parent_id")
|
||||
parent Industry? @relation("IndustryHierarchy", fields: [parentId], references: [id])
|
||||
children Industry[] @relation("IndustryHierarchy")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
// Relations
|
||||
useCases UseCaseIndustry[]
|
||||
|
||||
@@index([parentId])
|
||||
@@index([slug])
|
||||
@@map("industries")
|
||||
}
|
||||
|
||||
/// Category - categories for use case organization (AI-populated)
|
||||
model Category {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(100)
|
||||
slug String @unique @db.VarChar(100)
|
||||
type String @map("type") @db.VarChar(50) // 'functional', 'business-process', 'technical'
|
||||
description String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
// Relations
|
||||
useCases UseCaseCategory[]
|
||||
|
||||
@@index([type])
|
||||
@@index([slug])
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
/// UseCase - auto-generated marketing content from qualifying scenarios
|
||||
model UseCase {
|
||||
id String @id @default(cuid())
|
||||
scenarioId String @unique @map("scenario_id")
|
||||
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
||||
|
||||
// SEO-friendly slug
|
||||
slug String @db.VarChar(200)
|
||||
|
||||
// AI-generated marketing content
|
||||
marketingTitle String @map("marketing_title") @db.VarChar(200)
|
||||
marketingDesc String @map("marketing_desc") @db.Text
|
||||
roiEstimate String? @map("roi_estimate") @db.Text // "Saves ~10 hours/week"
|
||||
businessValue String? @map("business_value") @db.Text // Human-readable benefit
|
||||
problemStatement String? @map("problem_statement") @db.Text // What problem it solves
|
||||
solutionNarrative String? @map("solution_narrative") @db.Text // How it works
|
||||
|
||||
// Ranking (computed nightly)
|
||||
rankScore Float @default(0) @map("rank_score")
|
||||
lastRankedAt DateTime @map("last_ranked_at")
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
lastRegeneratedAt DateTime? @map("last_regenerated_at")
|
||||
|
||||
// Relations
|
||||
personas UseCasePersona[]
|
||||
industries UseCaseIndustry[]
|
||||
categories UseCaseCategory[]
|
||||
socialProof SocialProof?
|
||||
|
||||
@@index([scenarioId])
|
||||
@@index([slug])
|
||||
@@index([rankScore])
|
||||
@@index([lastRankedAt])
|
||||
@@index([lastRegeneratedAt])
|
||||
@@map("use_cases")
|
||||
}
|
||||
|
||||
/// UseCasePersona - junction table for UseCase <-> Persona
|
||||
model UseCasePersona {
|
||||
useCaseId String @map("use_case_id")
|
||||
personaId String @map("persona_id")
|
||||
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
|
||||
persona Persona @relation(fields: [personaId], references: [id], onDelete: Cascade)
|
||||
relevance Float? @map("relevance") // Future: relevance score for this persona
|
||||
|
||||
@@id([useCaseId, personaId])
|
||||
@@index([useCaseId])
|
||||
@@index([personaId])
|
||||
@@map("use_case_personas")
|
||||
}
|
||||
|
||||
/// UseCaseIndustry - junction table for UseCase <-> Industry
|
||||
model UseCaseIndustry {
|
||||
useCaseId String @map("use_case_id")
|
||||
industryId String @map("industry_id")
|
||||
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
|
||||
industry Industry @relation(fields: [industryId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([useCaseId, industryId])
|
||||
@@index([useCaseId])
|
||||
@@index([industryId])
|
||||
@@map("use_case_industries")
|
||||
}
|
||||
|
||||
/// UseCaseCategory - junction table for UseCase <-> Category
|
||||
model UseCaseCategory {
|
||||
useCaseId String @map("use_case_id")
|
||||
categoryId String @map("category_id")
|
||||
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
|
||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([useCaseId, categoryId])
|
||||
@@index([useCaseId])
|
||||
@@index([categoryId])
|
||||
@@map("use_case_categories")
|
||||
}
|
||||
|
||||
/// SocialProof - cached metrics from scenario runs for use cases
|
||||
model SocialProof {
|
||||
id String @id @default(cuid())
|
||||
useCaseId String @unique @map("use_case_id")
|
||||
useCase UseCase @relation(fields: [useCaseId], references: [id], onDelete: Cascade)
|
||||
|
||||
qualityScore Float @map("quality_score")
|
||||
totalRuns Int @map("total_runs")
|
||||
consecutivePasses Int @map("consecutive_passes")
|
||||
lastRunStatus String? @map("last_run_status") @db.VarChar(20)
|
||||
lastRunAt DateTime? @map("last_run_at")
|
||||
|
||||
// Computed display fields
|
||||
successRate Float? @map("success_rate")
|
||||
lastRunAgo String? @map("last_run_ago") // "2 days ago"
|
||||
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@index([useCaseId])
|
||||
@@index([qualityScore])
|
||||
@@index([totalRuns])
|
||||
@@map("social_proofs")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { prisma } from './client';
|
||||
export * from '@prisma/client';
|
||||
export { prisma } from './client';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { http, HttpResponse } from 'msw';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
export const handlers = [
|
||||
http.get('/api/tools', () => {
|
||||
|
|
|
|||
|
|
@ -5,44 +5,40 @@
|
|||
|
||||
// Changes feed
|
||||
export {
|
||||
fetchChanges,
|
||||
type Change,
|
||||
type ChangesFeedResponse,
|
||||
type FetchChangesOptions,
|
||||
type FetchChangesResult,
|
||||
fetchChanges,
|
||||
} from './changes';
|
||||
|
||||
// Keyword search
|
||||
export {
|
||||
searchByKeyword,
|
||||
searchAllByKeyword,
|
||||
type SearchPackage,
|
||||
type SearchResult,
|
||||
type SearchResponse,
|
||||
type SearchByKeywordOptions,
|
||||
} from './search';
|
||||
|
||||
// Package metadata
|
||||
export {
|
||||
fetchPackageMetadata,
|
||||
fetchLatestPackageVersion,
|
||||
fetchLatestPackageWithMetadata,
|
||||
fetchPackageTpmjsField,
|
||||
type PackageVersion,
|
||||
type PackageMetadata,
|
||||
type PackageVersionWithReadme,
|
||||
} from './package';
|
||||
|
||||
// Download statistics
|
||||
export { fetchDownloadStats, fetchBulkDownloadStats, type DownloadsResponse } from './stats';
|
||||
|
||||
// GitHub statistics
|
||||
export {
|
||||
fetchGitHubStars,
|
||||
fetchGitHubStarsFromRepository,
|
||||
parseGitHubUrl,
|
||||
type GitHubRepoResponse,
|
||||
parseGitHubUrl,
|
||||
} from './github';
|
||||
|
||||
// Package metadata
|
||||
export {
|
||||
fetchLatestPackageVersion,
|
||||
fetchLatestPackageWithMetadata,
|
||||
fetchPackageMetadata,
|
||||
fetchPackageTpmjsField,
|
||||
type PackageMetadata,
|
||||
type PackageVersion,
|
||||
type PackageVersionWithReadme,
|
||||
} from './package';
|
||||
// Rate limiting
|
||||
export { delay, RateLimiter, npmRateLimiter, retryWithBackoff } from './rate-limiter';
|
||||
export { delay, npmRateLimiter, RateLimiter, retryWithBackoff } from './rate-limiter';
|
||||
// Keyword search
|
||||
export {
|
||||
type SearchByKeywordOptions,
|
||||
type SearchPackage,
|
||||
type SearchResponse,
|
||||
type SearchResult,
|
||||
searchAllByKeyword,
|
||||
searchByKeyword,
|
||||
} from './search';
|
||||
// Download statistics
|
||||
export { type DownloadsResponse, fetchBulkDownloadStats, fetchDownloadStats } from './stats';
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './executor';
|
||||
export * from './types';
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ export const enrichCommand = new Command('enrich')
|
|||
|
||||
const processor = new BatchProcessor({
|
||||
dbPath: options.db,
|
||||
batchSize: Number.parseInt(options.batch),
|
||||
concurrency: Number.parseInt(options.concurrency),
|
||||
batchSize: Number.parseInt(options.batch, 10),
|
||||
concurrency: Number.parseInt(options.concurrency, 10),
|
||||
costLimitUsd: Number.parseFloat(options.costLimit),
|
||||
model: options.model,
|
||||
onProgress: (processed, total, cost) => {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const exportCommand = new Command('export')
|
|||
try {
|
||||
const db = getDatabase(options.db);
|
||||
const minQuality = Number.parseFloat(options.minQuality);
|
||||
const limit = Number.parseInt(options.limit);
|
||||
const limit = Number.parseInt(options.limit, 10);
|
||||
|
||||
// Build query conditions
|
||||
const conditions = [gte(toolIdeas.qualityScore, minQuality)];
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ export const generateCommand = new Command('generate')
|
|||
const spinner = ora('Generating tool skeletons...').start();
|
||||
|
||||
try {
|
||||
const count = Number.parseInt(options.count);
|
||||
const count = Number.parseInt(options.count, 10);
|
||||
const threshold = Number.parseFloat(options.threshold);
|
||||
const seed = Number.parseInt(options.seed);
|
||||
const seed = Number.parseInt(options.seed, 10);
|
||||
|
||||
spinner.text = `Generating up to ${count} skeletons (threshold: ${threshold})...`;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ vocabCommand
|
|||
getDatabase(options.db);
|
||||
|
||||
const counts = {
|
||||
categories: Number.parseInt(options.categories),
|
||||
verbs: Number.parseInt(options.verbs),
|
||||
objects: Number.parseInt(options.objects),
|
||||
contexts: Number.parseInt(options.contexts),
|
||||
qualifiers: Number.parseInt(options.qualifiers),
|
||||
categories: Number.parseInt(options.categories, 10),
|
||||
verbs: Number.parseInt(options.verbs, 10),
|
||||
objects: Number.parseInt(options.objects, 10),
|
||||
contexts: Number.parseInt(options.contexts, 10),
|
||||
qualifiers: Number.parseInt(options.qualifiers, 10),
|
||||
};
|
||||
|
||||
spinner.text = `Generating ${counts.categories} categories...`;
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ import { eq, inArray, sql } from 'drizzle-orm';
|
|||
import pLimit from 'p-limit';
|
||||
import { getDatabase } from '../db/client.js';
|
||||
import {
|
||||
type NewToolIdea,
|
||||
type ToolSkeleton,
|
||||
categories,
|
||||
contexts,
|
||||
type NewToolIdea,
|
||||
objects,
|
||||
processingErrors,
|
||||
type ToolSkeleton,
|
||||
toolIdeas,
|
||||
toolSkeletons,
|
||||
verbs,
|
||||
} from '../db/schema.js';
|
||||
import { type SkeletonWithRelations, createEnrichmentPrompt } from './prompts.js';
|
||||
import { createEnrichmentPrompt, type SkeletonWithRelations } from './prompts.js';
|
||||
import { EnrichedToolSchema } from './schemas.js';
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ import { z } from 'zod';
|
|||
import { getDatabase } from '../db/client.js';
|
||||
import {
|
||||
type Category,
|
||||
type ToolObject,
|
||||
type Verb,
|
||||
categories,
|
||||
categoryVerbAffinity,
|
||||
objects,
|
||||
type ToolObject,
|
||||
type Verb,
|
||||
verbObjectCompatibility,
|
||||
verbs,
|
||||
} from '../db/schema.js';
|
||||
|
||||
// Drizzle operators imported as needed
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -216,7 +217,7 @@ export async function seedCompatibilityRules(options: { dbPath?: string } = {})
|
|||
.onConflictDoNothing()
|
||||
.run();
|
||||
voCount++;
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
|
|
@ -241,7 +242,7 @@ export async function seedCompatibilityRules(options: { dbPath?: string } = {})
|
|||
.onConflictDoNothing()
|
||||
.run();
|
||||
cvCount++;
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
|
|
@ -300,7 +301,7 @@ export function loadCompatibilityRules(db: ReturnType<typeof getDatabase>) {
|
|||
cvRules.set(cat.name, { verbs: new Set(), score: rule.score });
|
||||
}
|
||||
|
||||
cvRules.get(cat.name)!.verbs.add(verb.name);
|
||||
cvRules.get(cat.name)?.verbs.add(verb.name);
|
||||
}
|
||||
|
||||
return { voRules, cvRules };
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { getDatabase } from '../db/client.js';
|
|||
import {
|
||||
type Category,
|
||||
type Context,
|
||||
type NewToolSkeleton,
|
||||
type ToolObject,
|
||||
type Verb,
|
||||
categories,
|
||||
contexts,
|
||||
type NewToolSkeleton,
|
||||
objects,
|
||||
type ToolObject,
|
||||
toolSkeletons,
|
||||
type Verb,
|
||||
verbs,
|
||||
} from '../db/schema.js';
|
||||
import { calculateCompatibilityScore, loadCompatibilityRules } from './compatibility.js';
|
||||
|
|
@ -212,7 +212,7 @@ export async function generateSkeletons(options: {
|
|||
try {
|
||||
db.insert(toolSkeletons).values(values).onConflictDoNothing().run();
|
||||
inserted += batch.length;
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Some might be duplicates
|
||||
for (const v of values) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
|
|||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
|
|
@ -334,7 +334,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
|
|||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
|
|
@ -352,7 +352,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
|
|||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
|
|
@ -369,7 +369,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
|
|||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
|
|
@ -386,7 +386,7 @@ export async function seedVocabulary(options: SeedVocabularyOptions = {}) {
|
|||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
// Database
|
||||
export { getDatabase } from './db/client.js';
|
||||
export * from './db/schema.js';
|
||||
|
||||
// Generators
|
||||
export { seedVocabulary, getVocabularyStats } from './generators/vocabulary.js';
|
||||
export {
|
||||
generateVerbObjectRules,
|
||||
generateCategoryVerbRules,
|
||||
calculateCompatibilityScore,
|
||||
seedCompatibilityRules,
|
||||
loadCompatibilityRules,
|
||||
} from './generators/compatibility.js';
|
||||
export { generateSkeletons, getSkeletonStats } from './generators/skeleton-generator.js';
|
||||
export type { BatchProcessorOptions } from './enrichment/batch-processor.js';
|
||||
export { BatchProcessor, getEnrichmentStats } from './enrichment/batch-processor.js';
|
||||
export { createBatchEnrichmentPrompt, createEnrichmentPrompt } from './enrichment/prompts.js';
|
||||
|
||||
// Enrichment
|
||||
export * from './enrichment/schemas.js';
|
||||
export { createEnrichmentPrompt, createBatchEnrichmentPrompt } from './enrichment/prompts.js';
|
||||
export { BatchProcessor, getEnrichmentStats } from './enrichment/batch-processor.js';
|
||||
export type { BatchProcessorOptions } from './enrichment/batch-processor.js';
|
||||
export {
|
||||
calculateCompatibilityScore,
|
||||
generateCategoryVerbRules,
|
||||
generateVerbObjectRules,
|
||||
loadCompatibilityRules,
|
||||
seedCompatibilityRules,
|
||||
} from './generators/compatibility.js';
|
||||
export { generateSkeletons, getSkeletonStats } from './generators/skeleton-generator.js';
|
||||
// Generators
|
||||
export { getVocabularyStats, seedVocabulary } from './generators/vocabulary.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { TPMJS_CATEGORIES, TpmjsToolDefinitionSchema } from '@tpmjs/types/tpmjs';
|
||||
import type { TpmjsCategory } from '@tpmjs/types/tpmjs';
|
||||
import { TPMJS_CATEGORIES, TpmjsToolDefinitionSchema } from '@tpmjs/types/tpmjs';
|
||||
import type { ToolDefinition } from '../types.js';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
* AI SDK tools for emoji conversion and mood detection
|
||||
*/
|
||||
|
||||
export { textToEmoji } from './tools/textToEmoji.js';
|
||||
export { emojiMood } from './tools/emojiMood.js';
|
||||
export { textToEmoji } from './tools/textToEmoji.js';
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
* Tools for formatting and manipulating markdown text
|
||||
*/
|
||||
|
||||
export { markdownToPlainText } from './tools/markdownToPlainText.js';
|
||||
export { formatMarkdownTable } from './tools/formatMarkdownTable.js';
|
||||
export { markdownToPlainText } from './tools/markdownToPlainText.js';
|
||||
|
|
|
|||
|
|
@ -52,13 +52,13 @@ export const markdownToPlainText = tool({
|
|||
plainText = plainText.replace(/```[\s\S]*?```/g, '');
|
||||
|
||||
// Remove horizontal rules (---, ***)
|
||||
plainText = plainText.replace(/^[\*\-_]{3,}$/gm, '');
|
||||
plainText = plainText.replace(/^[*\-_]{3,}$/gm, '');
|
||||
|
||||
// Remove blockquotes (>)
|
||||
plainText = plainText.replace(/^>\s*/gm, '');
|
||||
|
||||
// Remove list markers (-, *, +, 1.)
|
||||
plainText = plainText.replace(/^[\s]*[-\*\+]\s+/gm, '');
|
||||
plainText = plainText.replace(/^[\s]*[-*+]\s+/gm, '');
|
||||
plainText = plainText.replace(/^[\s]*\d+\.\s+/gm, '');
|
||||
|
||||
// Handle line breaks
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function extractDemographics(data: Record<string, unknown>): AudiencePersona['de
|
|||
// Extract age range
|
||||
if (data.age) {
|
||||
const age = Number(data.age);
|
||||
if (!isNaN(age)) {
|
||||
if (!Number.isNaN(age)) {
|
||||
if (age < 18) demographics.ageRange = 'Under 18';
|
||||
else if (age < 25) demographics.ageRange = '18-24';
|
||||
else if (age < 35) demographics.ageRange = '25-34';
|
||||
|
|
@ -237,7 +237,7 @@ function generateMarketingImplications(
|
|||
}
|
||||
|
||||
// Add age-specific recommendations
|
||||
const ageNum = Number.parseInt(demographics.ageRange.split('-')[0] || '0');
|
||||
const ageNum = Number.parseInt(demographics.ageRange.split('-')[0] || '0', 10);
|
||||
if (ageNum < 35) {
|
||||
contentRecommendations.push('Use short-form video content (TikTok, Reels)');
|
||||
} else if (ageNum >= 35 && ageNum < 55) {
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ function calculateDateRange(duration: string): { startDate: Date; endDate: Date;
|
|||
|
||||
if (durationLower.includes('week')) {
|
||||
const weekMatch = durationLower.match(/(\d+)\s*week/);
|
||||
weeks = weekMatch?.[1] ? Number.parseInt(weekMatch[1]) : 1;
|
||||
weeks = weekMatch?.[1] ? Number.parseInt(weekMatch[1], 10) : 1;
|
||||
endDate.setDate(endDate.getDate() + weeks * 7);
|
||||
} else if (durationLower.includes('month')) {
|
||||
const monthMatch = durationLower.match(/(\d+)\s*month/);
|
||||
const months = monthMatch?.[1] ? Number.parseInt(monthMatch[1]) : 1;
|
||||
const months = monthMatch?.[1] ? Number.parseInt(monthMatch[1], 10) : 1;
|
||||
endDate.setMonth(endDate.getMonth() + months);
|
||||
weeks = Math.ceil((endDate.getTime() - startDate.getTime()) / (7 * 24 * 60 * 60 * 1000));
|
||||
} else if (durationLower.includes('quarter')) {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ const PATTERNS = {
|
|||
|
||||
// Authentication Credentials
|
||||
apiKey: {
|
||||
regex: /\b(?:api[_-]?key|apikey|access[_-]?token)[\s:=]*['"]?[A-Za-z0-9_\-]{20,}['"]?/gi,
|
||||
regex: /\b(?:api[_-]?key|apikey|access[_-]?token)[\s:=]*['"]?[A-Za-z0-9_-]{20,}['"]?/gi,
|
||||
severity: 'critical' as const,
|
||||
type: 'API Key',
|
||||
description: 'API key or access token detected',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// tsup.config.ts
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
var tsup_config_default = defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
|
|
|
|||
|
|
@ -139,21 +139,21 @@ function detectDuplicates(pkg: PackageJson): DependencyIssue[] {
|
|||
if (!packageVersions.has(name)) {
|
||||
packageVersions.set(name, []);
|
||||
}
|
||||
packageVersions.get(name)!.push({ version, type: 'dependencies' });
|
||||
packageVersions.get(name)?.push({ version, type: 'dependencies' });
|
||||
}
|
||||
|
||||
for (const [name, version] of Object.entries(devDeps)) {
|
||||
if (!packageVersions.has(name)) {
|
||||
packageVersions.set(name, []);
|
||||
}
|
||||
packageVersions.get(name)!.push({ version, type: 'devDependencies' });
|
||||
packageVersions.get(name)?.push({ version, type: 'devDependencies' });
|
||||
}
|
||||
|
||||
for (const [name, version] of Object.entries(peerDeps)) {
|
||||
if (!packageVersions.has(name)) {
|
||||
packageVersions.set(name, []);
|
||||
}
|
||||
packageVersions.get(name)!.push({ version, type: 'peerDependencies' });
|
||||
packageVersions.get(name)?.push({ version, type: 'peerDependencies' });
|
||||
}
|
||||
|
||||
// Find duplicates with different versions
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ const CATEGORY_KEYWORDS: Record<string, string[]> = {
|
|||
* Determines the category for a FAQ based on question and answer content
|
||||
*/
|
||||
function categorize(question: string, answer: string): string {
|
||||
const text = (question + ' ' + answer).toLowerCase();
|
||||
const text = `${question} ${answer}`.toLowerCase();
|
||||
|
||||
// Check each category's keywords
|
||||
for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
|
||||
|
|
|
|||
|
|
@ -3,64 +3,65 @@
|
|||
* This file provides metadata for the blocks validator
|
||||
*/
|
||||
import {
|
||||
// Topology
|
||||
executeTopology,
|
||||
// Sessions
|
||||
listSessions,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
// Messages
|
||||
addMessage,
|
||||
clearMessages,
|
||||
// Prompts
|
||||
listPrompts,
|
||||
createApiKey,
|
||||
createPrompt,
|
||||
getPrompt,
|
||||
updatePrompt,
|
||||
deletePrompt,
|
||||
incrementPromptUsage,
|
||||
// User
|
||||
getUserProfile,
|
||||
updateUserProfile,
|
||||
getUserStats,
|
||||
// Env Vars
|
||||
listEnvVars,
|
||||
setEnvVar,
|
||||
createSession,
|
||||
deleteApiKey,
|
||||
deleteEnvVar,
|
||||
// Files
|
||||
uploadFile,
|
||||
getFile,
|
||||
deleteFile,
|
||||
// Models
|
||||
listModels,
|
||||
// Logs
|
||||
getExecutionLogs,
|
||||
// Metrics
|
||||
getAgentMetrics,
|
||||
// Tools
|
||||
listTools,
|
||||
deletePrompt,
|
||||
deleteSession,
|
||||
describeTool,
|
||||
executeTool,
|
||||
// Prompt Generation
|
||||
generatePrompt,
|
||||
improvePrompt,
|
||||
// Topology
|
||||
executeTopology,
|
||||
// Export/Import
|
||||
exportData,
|
||||
importData,
|
||||
// Prompt Generation
|
||||
generatePrompt,
|
||||
// Metrics
|
||||
getAgentMetrics,
|
||||
// Logs
|
||||
getExecutionLogs,
|
||||
getFile,
|
||||
getPrompt,
|
||||
getSession,
|
||||
getStats,
|
||||
// User
|
||||
getUserProfile,
|
||||
getUserStats,
|
||||
// Public
|
||||
healthCheck,
|
||||
getStats,
|
||||
importData,
|
||||
improvePrompt,
|
||||
incrementPromptUsage,
|
||||
// API Keys
|
||||
listApiKeys,
|
||||
createApiKey,
|
||||
deleteApiKey,
|
||||
// Env Vars
|
||||
listEnvVars,
|
||||
// Models
|
||||
listModels,
|
||||
// Prompts
|
||||
listPrompts,
|
||||
// Sessions
|
||||
listSessions,
|
||||
// Tools
|
||||
listTools,
|
||||
setEnvVar,
|
||||
updatePrompt,
|
||||
updateSession,
|
||||
updateUserProfile,
|
||||
// Files
|
||||
uploadFile,
|
||||
} from './src/index.js';
|
||||
|
||||
export const block = {
|
||||
name: 'hllm',
|
||||
description: 'HLLM API client tools for AI agents. Manage topologies, sessions, prompts, files, and more.',
|
||||
description:
|
||||
'HLLM API client tools for AI agents. Manage topologies, sessions, prompts, files, and more.',
|
||||
tools: {
|
||||
// Topology
|
||||
executeTopology,
|
||||
|
|
|
|||
|
|
@ -87,9 +87,7 @@ function handleApiError(status: number, errorText: string): never {
|
|||
case 400:
|
||||
throw new Error(`Bad request: ${errorText}`);
|
||||
case 401:
|
||||
throw new Error(
|
||||
'Authentication failed: Invalid API key. Ensure HLLM_API_KEY is correct.'
|
||||
);
|
||||
throw new Error('Authentication failed: Invalid API key. Ensure HLLM_API_KEY is correct.');
|
||||
case 403:
|
||||
throw new Error(`Access forbidden: ${errorText}`);
|
||||
case 404:
|
||||
|
|
@ -390,11 +388,7 @@ export const updateSession = tool({
|
|||
}),
|
||||
async execute(input: UpdateSessionInput): Promise<Session> {
|
||||
const { sessionId, ...body } = input;
|
||||
return apiRequest<Session>(
|
||||
'PATCH',
|
||||
`/chat/sessions/${encodeURIComponent(sessionId)}`,
|
||||
body
|
||||
);
|
||||
return apiRequest<Session>('PATCH', `/chat/sessions/${encodeURIComponent(sessionId)}`, body);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -629,11 +623,7 @@ export const getPrompt = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: { promptId: string }): Promise<Prompt> {
|
||||
return apiRequest<Prompt>(
|
||||
'GET',
|
||||
`/prompts/${encodeURIComponent(input.promptId)}`,
|
||||
undefined
|
||||
);
|
||||
return apiRequest<Prompt>('GET', `/prompts/${encodeURIComponent(input.promptId)}`, undefined);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -685,11 +675,7 @@ export const updatePrompt = tool({
|
|||
}),
|
||||
async execute(input: UpdatePromptInput): Promise<Prompt> {
|
||||
const { promptId, ...body } = input;
|
||||
return apiRequest<Prompt>(
|
||||
'PATCH',
|
||||
`/prompts/${encodeURIComponent(promptId)}`,
|
||||
body
|
||||
);
|
||||
return apiRequest<Prompt>('PATCH', `/prompts/${encodeURIComponent(promptId)}`, body);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -991,11 +977,7 @@ export const getFile = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: { fileId: string }): Promise<FileInfo> {
|
||||
return apiRequest<FileInfo>(
|
||||
'GET',
|
||||
`/files/${encodeURIComponent(input.fileId)}`,
|
||||
undefined
|
||||
);
|
||||
return apiRequest<FileInfo>('GET', `/files/${encodeURIComponent(input.fileId)}`, undefined);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1134,11 +1116,14 @@ export interface AgentMetrics {
|
|||
input: number;
|
||||
output: number;
|
||||
};
|
||||
topologyBreakdown: Record<string, {
|
||||
count: number;
|
||||
avgLatency: number;
|
||||
successRate: number;
|
||||
}>;
|
||||
topologyBreakdown: Record<
|
||||
string,
|
||||
{
|
||||
count: number;
|
||||
avgLatency: number;
|
||||
successRate: number;
|
||||
}
|
||||
>;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
}
|
||||
|
|
@ -1224,11 +1209,7 @@ export const describeTool = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: { toolId: string }): Promise<ToolInfo> {
|
||||
return apiRequest<ToolInfo>(
|
||||
'POST',
|
||||
'/tpmjs/describe',
|
||||
{ toolId: input.toolId }
|
||||
);
|
||||
return apiRequest<ToolInfo>('POST', '/tpmjs/describe', { toolId: input.toolId });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1265,11 +1246,10 @@ export const executeTool = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ExecuteToolInput): Promise<ExecuteToolResult> {
|
||||
return apiRequest<ExecuteToolResult>(
|
||||
'POST',
|
||||
'/tpmjs/execute',
|
||||
{ toolId: input.toolId, parameters: input.parameters }
|
||||
);
|
||||
return apiRequest<ExecuteToolResult>('POST', '/tpmjs/execute', {
|
||||
toolId: input.toolId,
|
||||
parameters: input.parameters,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export interface HtmlToMarkdownResult {
|
|||
function countWords(text: string): number {
|
||||
// Remove markdown formatting characters for accurate word count
|
||||
const plainText = text
|
||||
.replace(/[#*_`~\[\]()]/g, ' ') // Remove markdown syntax
|
||||
.replace(/[#*_`~[\]()]/g, ' ') // Remove markdown syntax
|
||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||
.trim();
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ function extractVendorInfo(text: string): VendorInfo {
|
|||
}
|
||||
|
||||
// Extract phone number
|
||||
const phoneMatch = text.match(/(?:phone|tel|p):?\s*([0-9\-\(\)\s]{10,})/i);
|
||||
const phoneMatch = text.match(/(?:phone|tel|p):?\s*([0-9\-()\s]{10,})/i);
|
||||
const phone = phoneMatch?.[1]?.trim();
|
||||
|
||||
// Extract email
|
||||
|
|
@ -109,7 +109,7 @@ function extractVendorInfo(text: string): VendorInfo {
|
|||
const email = emailMatch?.[1];
|
||||
|
||||
// Extract tax ID
|
||||
const taxIdMatch = text.match(/(?:tax\s*id|ein|vat):?\s*([0-9\-]+)/i);
|
||||
const taxIdMatch = text.match(/(?:tax\s*id|ein|vat):?\s*([0-9-]+)/i);
|
||||
const taxId = taxIdMatch?.[1];
|
||||
|
||||
// Extract address (simplified)
|
||||
|
|
@ -133,12 +133,12 @@ function extractMetadata(text: string): {
|
|||
invoiceDate?: string;
|
||||
} {
|
||||
// Extract invoice number
|
||||
const invoiceNumMatch = text.match(/(?:invoice|inv)\s*(?:#|no|number):?\s*([A-Z0-9\-]+)/i);
|
||||
const invoiceNumMatch = text.match(/(?:invoice|inv)\s*(?:#|no|number):?\s*([A-Z0-9-]+)/i);
|
||||
const invoiceNumber = invoiceNumMatch?.[1];
|
||||
|
||||
// Extract invoice date
|
||||
const dateMatch = text.match(
|
||||
/(?:date|dated|invoice\s+date):?\s*([0-9]{1,2}[\/\-][0-9]{1,2}[\/\-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
|
||||
/(?:date|dated|invoice\s+date):?\s*([0-9]{1,2}[/-][0-9]{1,2}[/-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
|
||||
);
|
||||
const invoiceDate = dateMatch?.[1];
|
||||
|
||||
|
|
@ -267,12 +267,12 @@ function extractTotals(text: string): {
|
|||
*/
|
||||
function extractPaymentTerms(text: string): PaymentTerms | undefined {
|
||||
const dueDateMatch = text.match(
|
||||
/(?:due\s*date|payment\s*due):?\s*([0-9]{1,2}[\/\-][0-9]{1,2}[\/\-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
|
||||
/(?:due\s*date|payment\s*due):?\s*([0-9]{1,2}[/-][0-9]{1,2}[/-][0-9]{2,4}|[A-Z][a-z]+\s+[0-9]{1,2},?\s+[0-9]{4})/i
|
||||
);
|
||||
const dueDate = dueDateMatch?.[1];
|
||||
|
||||
const netDaysMatch = text.match(/(?:net|due\s+in)\s+(\d+)\s*(?:days?)/i);
|
||||
const netDays = netDaysMatch?.[1] ? Number.parseInt(netDaysMatch[1]) : undefined;
|
||||
const netDays = netDaysMatch?.[1] ? Number.parseInt(netDaysMatch[1], 10) : undefined;
|
||||
|
||||
if (!dueDate && !netDays) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -56,13 +56,13 @@ function extractNetDays(text: string): number | null {
|
|||
|
||||
// Match "Net XX" or "Net XX days"
|
||||
const netMatch = normalizedText.match(/net\s+(\d+)(?:\s+days?)?/i);
|
||||
if (netMatch && netMatch[1]) {
|
||||
if (netMatch?.[1]) {
|
||||
return Number.parseInt(netMatch[1], 10);
|
||||
}
|
||||
|
||||
// Match "XX days" in payment terms context
|
||||
const daysMatch = normalizedText.match(/(?:payment\s+)?(?:due\s+)?(?:in\s+)?(\d+)\s+days/i);
|
||||
if (daysMatch && daysMatch[1]) {
|
||||
if (daysMatch?.[1]) {
|
||||
return Number.parseInt(daysMatch[1], 10);
|
||||
}
|
||||
|
||||
|
|
@ -84,15 +84,15 @@ function extractNetDays(text: string): number | null {
|
|||
function extractDueDate(text: string): string | null {
|
||||
// Match common date formats: MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD
|
||||
const datePatterns = [
|
||||
/due(?:\s+date)?:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
|
||||
/payment\s+due:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
|
||||
/due\s+by:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
|
||||
/due\s+on:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
|
||||
/due(?:\s+date)?:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
|
||||
/payment\s+due:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
|
||||
/due\s+by:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
|
||||
/due\s+on:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
|
||||
];
|
||||
|
||||
for (const pattern of datePatterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match && match[1]) {
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ function extractDueDate(text: string): string | null {
|
|||
const writtenDateMatch = text.match(
|
||||
/due\s+(?:date|on|by)?:?\s*(\w+\s+\d{1,2},?\s+\d{4}|\d{1,2}\s+\w+\s+\d{4})/i
|
||||
);
|
||||
if (writtenDateMatch && writtenDateMatch[1]) {
|
||||
if (writtenDateMatch?.[1]) {
|
||||
return writtenDateMatch[1];
|
||||
}
|
||||
|
||||
|
|
@ -113,14 +113,14 @@ function extractDueDate(text: string): string | null {
|
|||
*/
|
||||
function extractInvoiceDate(text: string): string | null {
|
||||
const datePatterns = [
|
||||
/invoice\s+date:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
|
||||
/date:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
|
||||
/dated:?\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/i,
|
||||
/invoice\s+date:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
|
||||
/date:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
|
||||
/dated:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})/i,
|
||||
];
|
||||
|
||||
for (const pattern of datePatterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match && match[1]) {
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
|
@ -133,14 +133,14 @@ function extractInvoiceDate(text: string): string | null {
|
|||
*/
|
||||
function extractInvoiceNumber(text: string): string | null {
|
||||
const patterns = [
|
||||
/invoice\s+(?:number|#|no\.?):?\s*([A-Z0-9\-]+)/i,
|
||||
/invoice:?\s+([A-Z0-9\-]+)/i,
|
||||
/#\s*([A-Z0-9\-]+)/,
|
||||
/invoice\s+(?:number|#|no\.?):?\s*([A-Z0-9-]+)/i,
|
||||
/invoice:?\s+([A-Z0-9-]+)/i,
|
||||
/#\s*([A-Z0-9-]+)/,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match && match[1]) {
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
|
@ -169,7 +169,7 @@ function extractTotalAmount(text: string): { amount: number | null; currency: st
|
|||
if (amountStr) {
|
||||
const amount = Number.parseFloat(amountStr.replace(/,/g, ''));
|
||||
|
||||
if (!isNaN(amount)) {
|
||||
if (!Number.isNaN(amount)) {
|
||||
return { amount, currency };
|
||||
}
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ function extractLateFee(text: string): LateFee | null {
|
|||
const percentageMatch = normalizedText.match(
|
||||
/([\d.]+)%\s*(?:per\s+month|monthly|late\s+fee|interest)/i
|
||||
);
|
||||
if (percentageMatch && percentageMatch[1]) {
|
||||
if (percentageMatch?.[1]) {
|
||||
return {
|
||||
type: 'monthly',
|
||||
amount: Number.parseFloat(percentageMatch[1]),
|
||||
|
|
@ -199,7 +199,7 @@ function extractLateFee(text: string): LateFee | null {
|
|||
|
||||
// Match daily percentage: "0.05% per day"
|
||||
const dailyMatch = normalizedText.match(/([\d.]+)%\s*(?:per\s+day|daily)/i);
|
||||
if (dailyMatch && dailyMatch[1]) {
|
||||
if (dailyMatch?.[1]) {
|
||||
return {
|
||||
type: 'daily',
|
||||
amount: Number.parseFloat(dailyMatch[1]),
|
||||
|
|
@ -209,7 +209,7 @@ function extractLateFee(text: string): LateFee | null {
|
|||
|
||||
// Match fixed fee: "$25 late fee" or "late fee of $50"
|
||||
const fixedMatch = text.match(/(?:late\s+fee|fee)(?:\s+of)?\s*\$?\s*([\d.]+)/i);
|
||||
if (fixedMatch && fixedMatch[1]) {
|
||||
if (fixedMatch?.[1]) {
|
||||
return {
|
||||
type: 'fixed',
|
||||
amount: Number.parseFloat(fixedMatch[1]),
|
||||
|
|
@ -227,7 +227,7 @@ function extractLateFee(text: string): LateFee | null {
|
|||
function extractEarlyPaymentDiscount(text: string): EarlyPaymentDiscount | null {
|
||||
// Match "2/10 Net 30" format (2% discount if paid within 10 days)
|
||||
const discountMatch = text.match(/(\d+)\/(\d+)\s+(?:net|n)\s+\d+/i);
|
||||
if (discountMatch && discountMatch[1] && discountMatch[2]) {
|
||||
if (discountMatch?.[1] && discountMatch[2]) {
|
||||
return {
|
||||
discountPercentage: Number.parseInt(discountMatch[1], 10),
|
||||
paymentDays: Number.parseInt(discountMatch[2], 10),
|
||||
|
|
@ -237,7 +237,7 @@ function extractEarlyPaymentDiscount(text: string): EarlyPaymentDiscount | null
|
|||
|
||||
// Match "X% discount if paid within Y days"
|
||||
const explicitMatch = text.match(/(\d+)%\s+discount\s+(?:if\s+paid\s+)?within\s+(\d+)\s+days/i);
|
||||
if (explicitMatch && explicitMatch[1] && explicitMatch[2]) {
|
||||
if (explicitMatch?.[1] && explicitMatch[2]) {
|
||||
return {
|
||||
discountPercentage: Number.parseInt(explicitMatch[1], 10),
|
||||
paymentDays: Number.parseInt(explicitMatch[2], 10),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { judgeConversation } from './src/index.js';
|
|||
|
||||
export const block = {
|
||||
name: 'judge',
|
||||
description: 'AI conversation quality judge. Evaluates AI SDK messages across 10 metrics with scores, reasoning, suggestions, and actionable improvements.',
|
||||
description:
|
||||
'AI conversation quality judge. Evaluates AI SDK messages across 10 metrics with scores, reasoning, suggestions, and actionable improvements.',
|
||||
tools: {
|
||||
judgeConversation,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export type EvaluationMetric = (typeof EVALUATION_METRICS)[number];
|
|||
* Metric descriptions for context
|
||||
*/
|
||||
export const METRIC_DESCRIPTIONS: Record<EvaluationMetric, string> = {
|
||||
taskCompletion: 'Did the AI make meaningful progress toward completing the user\'s request?',
|
||||
taskCompletion: "Did the AI make meaningful progress toward completing the user's request?",
|
||||
accuracy: 'Are the responses factually correct and free of hallucinations or errors?',
|
||||
relevance: 'Are responses directly addressing what the user asked for?',
|
||||
clarity: 'Are responses clear, well-structured, and easy to understand?',
|
||||
|
|
@ -51,16 +51,16 @@ export const METRIC_DESCRIPTIONS: Record<EvaluationMetric, string> = {
|
|||
* Metric weights for overall score calculation
|
||||
*/
|
||||
export const METRIC_WEIGHTS: Record<EvaluationMetric, number> = {
|
||||
taskCompletion: 2.0, // Most important - did we do what was asked?
|
||||
taskCompletion: 2.0, // Most important - did we do what was asked?
|
||||
userIntentAlignment: 1.5, // Critical - understanding the user
|
||||
completeness: 1.5, // Important - full coverage
|
||||
accuracy: 1.5, // Important - correctness
|
||||
progress: 1.2, // Catches loops
|
||||
actionability: 1.0, // Usability
|
||||
relevance: 1.0, // On-topic
|
||||
errorHandling: 1.0, // Resilience
|
||||
clarity: 0.8, // Nice to have
|
||||
efficiency: 0.5, // Least critical
|
||||
completeness: 1.5, // Important - full coverage
|
||||
accuracy: 1.5, // Important - correctness
|
||||
progress: 1.2, // Catches loops
|
||||
actionability: 1.0, // Usability
|
||||
relevance: 1.0, // On-topic
|
||||
errorHandling: 1.0, // Resilience
|
||||
clarity: 0.8, // Nice to have
|
||||
efficiency: 0.5, // Least critical
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -127,7 +127,7 @@ interface JudgeInput {
|
|||
*/
|
||||
function detectLoops(messages: Message[]): { detected: boolean; details: string[] } {
|
||||
const details: string[] = [];
|
||||
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
||||
const assistantMessages = messages.filter((m) => m.role === 'assistant');
|
||||
|
||||
if (assistantMessages.length < 2) {
|
||||
return { detected: false, details: [] };
|
||||
|
|
@ -168,7 +168,7 @@ function detectLoops(messages: Message[]): { detected: boolean; details: string[
|
|||
|
||||
// Check for oscillation patterns
|
||||
if (assistantMessages.length >= 4) {
|
||||
const last4 = assistantMessages.slice(-4).map(m => m.content.slice(0, 100));
|
||||
const last4 = assistantMessages.slice(-4).map((m) => m.content.slice(0, 100));
|
||||
if (last4[0] === last4[2] && last4[1] === last4[3]) {
|
||||
details.push('Detected oscillation pattern in last 4 responses');
|
||||
}
|
||||
|
|
@ -186,10 +186,10 @@ function detectLoops(messages: Message[]): { detected: boolean; details: string[
|
|||
function extractUserRequest(messages: Message[], provided?: string): string {
|
||||
if (provided) return provided;
|
||||
|
||||
const userMessages = messages.filter(m => m.role === 'user');
|
||||
const userMessages = messages.filter((m) => m.role === 'user');
|
||||
if (userMessages.length === 0) return 'Unknown request';
|
||||
|
||||
return userMessages[0]!.content;
|
||||
return userMessages[0]?.content;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -216,7 +216,11 @@ function analyzeToolUsage(messages: Message[]): {
|
|||
if (msg.toolResults) {
|
||||
for (const result of msg.toolResults) {
|
||||
const resultStr = JSON.stringify(result.result).toLowerCase();
|
||||
if (resultStr.includes('error') || resultStr.includes('failed') || resultStr.includes('exception')) {
|
||||
if (
|
||||
resultStr.includes('error') ||
|
||||
resultStr.includes('failed') ||
|
||||
resultStr.includes('exception')
|
||||
) {
|
||||
failedCalls++;
|
||||
} else {
|
||||
successfulCalls++;
|
||||
|
|
@ -249,16 +253,26 @@ function evaluateMetric(
|
|||
let score = 5;
|
||||
let reason = '';
|
||||
|
||||
const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
|
||||
const assistantCount = messages.filter(m => m.role === 'assistant').length;
|
||||
const userCount = messages.filter(m => m.role === 'user').length;
|
||||
const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant');
|
||||
const assistantCount = messages.filter((m) => m.role === 'assistant').length;
|
||||
const userCount = messages.filter((m) => m.role === 'user').length;
|
||||
|
||||
switch (metric) {
|
||||
case 'taskCompletion': {
|
||||
// Check if there's evidence of task completion
|
||||
const lastContent = lastAssistant?.content.toLowerCase() || '';
|
||||
const completionIndicators = ['done', 'completed', 'finished', 'created', 'updated', 'fixed', 'resolved', 'here is', 'here\'s'];
|
||||
const hasCompletion = completionIndicators.some(ind => lastContent.includes(ind));
|
||||
const completionIndicators = [
|
||||
'done',
|
||||
'completed',
|
||||
'finished',
|
||||
'created',
|
||||
'updated',
|
||||
'fixed',
|
||||
'resolved',
|
||||
'here is',
|
||||
"here's",
|
||||
];
|
||||
const hasCompletion = completionIndicators.some((ind) => lastContent.includes(ind));
|
||||
|
||||
if (hasCompletion && toolStats.successfulCalls > 0) {
|
||||
score = 8;
|
||||
|
|
@ -285,7 +299,9 @@ function evaluateMetric(
|
|||
score = 3;
|
||||
reason = 'More failed tool calls than successful ones indicates potential accuracy issues';
|
||||
mustDos.push('Investigate and fix the failing tool calls');
|
||||
observations.push(`${toolStats.failedCalls} failed vs ${toolStats.successfulCalls} successful calls`);
|
||||
observations.push(
|
||||
`${toolStats.failedCalls} failed vs ${toolStats.successfulCalls} successful calls`
|
||||
);
|
||||
} else if (toolStats.successfulCalls > 0) {
|
||||
score = 7;
|
||||
reason = 'Successful tool calls suggest accurate execution';
|
||||
|
|
@ -300,10 +316,17 @@ function evaluateMetric(
|
|||
|
||||
case 'relevance': {
|
||||
// Simple heuristic: check if user request terms appear in responses
|
||||
const requestTerms = userRequest.toLowerCase().split(/\s+/).filter(t => t.length > 3);
|
||||
const responseText = messages.filter(m => m.role === 'assistant').map(m => m.content.toLowerCase()).join(' ');
|
||||
const matchedTerms = requestTerms.filter(t => responseText.includes(t));
|
||||
const relevanceRatio = requestTerms.length > 0 ? matchedTerms.length / requestTerms.length : 0.5;
|
||||
const requestTerms = userRequest
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((t) => t.length > 3);
|
||||
const responseText = messages
|
||||
.filter((m) => m.role === 'assistant')
|
||||
.map((m) => m.content.toLowerCase())
|
||||
.join(' ');
|
||||
const matchedTerms = requestTerms.filter((t) => responseText.includes(t));
|
||||
const relevanceRatio =
|
||||
requestTerms.length > 0 ? matchedTerms.length / requestTerms.length : 0.5;
|
||||
|
||||
score = Math.round(relevanceRatio * 10);
|
||||
if (score >= 7) {
|
||||
|
|
@ -320,7 +343,8 @@ function evaluateMetric(
|
|||
}
|
||||
|
||||
case 'clarity': {
|
||||
const totalWords = messages.filter(m => m.role === 'assistant')
|
||||
const totalWords = messages
|
||||
.filter((m) => m.role === 'assistant')
|
||||
.reduce((sum, m) => sum + m.content.split(/\s+/).length, 0);
|
||||
const avgWordsPerResponse = assistantCount > 0 ? totalWords / assistantCount : 0;
|
||||
|
||||
|
|
@ -360,11 +384,14 @@ function evaluateMetric(
|
|||
|
||||
case 'userIntentAlignment': {
|
||||
// Check if the conversation seems to understand the request
|
||||
const hasQuestions = messages.some(m => m.role === 'assistant' && m.content.includes('?'));
|
||||
const hasConfirmation = messages.some(m => m.role === 'assistant' &&
|
||||
(m.content.toLowerCase().includes('i understand') ||
|
||||
m.content.toLowerCase().includes('let me') ||
|
||||
m.content.toLowerCase().includes('i\'ll')));
|
||||
const hasQuestions = messages.some((m) => m.role === 'assistant' && m.content.includes('?'));
|
||||
const hasConfirmation = messages.some(
|
||||
(m) =>
|
||||
m.role === 'assistant' &&
|
||||
(m.content.toLowerCase().includes('i understand') ||
|
||||
m.content.toLowerCase().includes('let me') ||
|
||||
m.content.toLowerCase().includes("i'll"))
|
||||
);
|
||||
|
||||
if (hasConfirmation && !hasQuestions) {
|
||||
score = 8;
|
||||
|
|
@ -387,9 +414,14 @@ function evaluateMetric(
|
|||
|
||||
case 'actionability': {
|
||||
const lastContent = lastAssistant?.content || '';
|
||||
const hasCode = lastContent.includes('```') || lastContent.includes('function') || lastContent.includes('const ');
|
||||
const hasCode =
|
||||
lastContent.includes('```') ||
|
||||
lastContent.includes('function') ||
|
||||
lastContent.includes('const ');
|
||||
const hasSteps = /\d+\.\s/.test(lastContent) || lastContent.includes('- ');
|
||||
const hasFiles = toolStats.toolsUsed.some(t => t.toLowerCase().includes('write') || t.toLowerCase().includes('create'));
|
||||
const hasFiles = toolStats.toolsUsed.some(
|
||||
(t) => t.toLowerCase().includes('write') || t.toLowerCase().includes('create')
|
||||
);
|
||||
|
||||
if (hasFiles) {
|
||||
score = 9;
|
||||
|
|
@ -446,18 +478,29 @@ function evaluateMetric(
|
|||
score = 5;
|
||||
reason = 'Error handling status unclear';
|
||||
}
|
||||
observations.push(`${toolStats.failedCalls} failed, ${toolStats.successfulCalls} successful calls`);
|
||||
observations.push(
|
||||
`${toolStats.failedCalls} failed, ${toolStats.successfulCalls} successful calls`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'completeness': {
|
||||
// Check for common incompleteness indicators
|
||||
const lastContent = lastAssistant?.content.toLowerCase() || '';
|
||||
const incompleteIndicators = ['todo', 'later', 'next time', 'will add', 'placeholder', 'not yet', 'tbd', '...'];
|
||||
const hasIncomplete = incompleteIndicators.some(ind => lastContent.includes(ind));
|
||||
const incompleteIndicators = [
|
||||
'todo',
|
||||
'later',
|
||||
'next time',
|
||||
'will add',
|
||||
'placeholder',
|
||||
'not yet',
|
||||
'tbd',
|
||||
'...',
|
||||
];
|
||||
const hasIncomplete = incompleteIndicators.some((ind) => lastContent.includes(ind));
|
||||
|
||||
const completeIndicators = ['all done', 'complete', 'finished', 'everything', 'all set'];
|
||||
const hasComplete = completeIndicators.some(ind => lastContent.includes(ind));
|
||||
const hasComplete = completeIndicators.some((ind) => lastContent.includes(ind));
|
||||
|
||||
if (hasComplete && !hasIncomplete) {
|
||||
score = 9;
|
||||
|
|
@ -517,8 +560,8 @@ function determineVerdict(
|
|||
const threshold = strictMode ? 7 : 5;
|
||||
|
||||
// Critical failures that always result in retry/fail
|
||||
const taskCompletion = metrics.find(m => m.metric === 'taskCompletion')?.score || 0;
|
||||
const progress = metrics.find(m => m.metric === 'progress')?.score || 0;
|
||||
const taskCompletion = metrics.find((m) => m.metric === 'taskCompletion')?.score || 0;
|
||||
const progress = metrics.find((m) => m.metric === 'progress')?.score || 0;
|
||||
|
||||
if (loopDetected) {
|
||||
return { verdict: 'retry', reason: 'Loop detected - must try a different approach' };
|
||||
|
|
@ -550,9 +593,12 @@ function determineVerdict(
|
|||
/**
|
||||
* Generate conversation summary
|
||||
*/
|
||||
function generateSummary(messages: Message[], toolStats: ReturnType<typeof analyzeToolUsage>): string {
|
||||
const userCount = messages.filter(m => m.role === 'user').length;
|
||||
const assistantCount = messages.filter(m => m.role === 'assistant').length;
|
||||
function generateSummary(
|
||||
messages: Message[],
|
||||
toolStats: ReturnType<typeof analyzeToolUsage>
|
||||
): string {
|
||||
const userCount = messages.filter((m) => m.role === 'user').length;
|
||||
const assistantCount = messages.filter((m) => m.role === 'assistant').length;
|
||||
|
||||
return `Conversation with ${userCount} user message(s), ${assistantCount} assistant response(s), and ${toolStats.totalCalls} tool call(s) (${toolStats.successfulCalls} successful, ${toolStats.failedCalls} failed). Tools used: ${toolStats.toolsUsed.join(', ') || 'none'}.`;
|
||||
}
|
||||
|
|
@ -587,7 +633,8 @@ Returns a verdict (pass/retry/fail) with specific must-dos for any issues.`,
|
|||
properties: {
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'Array of AI SDK messages to evaluate. Each message should have role and content.',
|
||||
description:
|
||||
'Array of AI SDK messages to evaluate. Each message should have role and content.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
@ -665,21 +712,26 @@ Returns a verdict (pass/retry/fail) with specific must-dos for any issues.`,
|
|||
const toolStats = analyzeToolUsage(messages);
|
||||
|
||||
// Evaluate all metrics
|
||||
const metrics = EVALUATION_METRICS.map(metric =>
|
||||
const metrics = EVALUATION_METRICS.map((metric) =>
|
||||
evaluateMetric(metric, messages, userRequest, loopInfo, toolStats)
|
||||
);
|
||||
|
||||
const overallScore = calculateOverallScore(metrics);
|
||||
const { verdict, reason: verdictReason } = determineVerdict(metrics, overallScore, loopInfo.detected, strictMode);
|
||||
const { verdict, reason: verdictReason } = determineVerdict(
|
||||
metrics,
|
||||
overallScore,
|
||||
loopInfo.detected,
|
||||
strictMode
|
||||
);
|
||||
|
||||
// Aggregate must-dos and suggestions
|
||||
const allMustDos = metrics.flatMap(m => m.mustDos);
|
||||
const allSuggestions = metrics.flatMap(m => m.suggestions);
|
||||
const allMustDos = metrics.flatMap((m) => m.mustDos);
|
||||
const allSuggestions = metrics.flatMap((m) => m.suggestions);
|
||||
|
||||
// Identify critical issues (metrics with score <= 3)
|
||||
const criticalIssues = metrics
|
||||
.filter(m => m.score <= 3)
|
||||
.map(m => `${m.metric}: ${m.reason}`);
|
||||
.filter((m) => m.score <= 3)
|
||||
.map((m) => `${m.metric}: ${m.reason}`);
|
||||
|
||||
// Generate next steps based on verdict
|
||||
const nextSteps: string[] = [];
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function transpose(matrix: number[][]): number[][] {
|
|||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
for (let j = 0; j < cols; j++) {
|
||||
result[j]![i] = matrix[i]![j] ?? 0;
|
||||
result[j]![i] = matrix[i]?.[j] ?? 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -74,7 +74,7 @@ function matrixMultiply(a: number[][], b: number[][]): number[][] {
|
|||
for (let j = 0; j < bCols; j++) {
|
||||
let sum = 0;
|
||||
for (let k = 0; k < aCols; k++) {
|
||||
sum += (a[i]![k] ?? 0) * (b[k]![j] ?? 0);
|
||||
sum += (a[i]?.[k] ?? 0) * (b[k]?.[j] ?? 0);
|
||||
}
|
||||
result[i]![j] = sum;
|
||||
}
|
||||
|
|
@ -100,27 +100,27 @@ function invertMatrix(matrix: number[][]): number[][] {
|
|||
for (let i = 0; i < n; i++) {
|
||||
let maxRow = i;
|
||||
for (let k = i + 1; k < n; k++) {
|
||||
if (Math.abs(augmented[k]![i] ?? 0) > Math.abs(augmented[maxRow]![i] ?? 0)) {
|
||||
if (Math.abs(augmented[k]?.[i] ?? 0) > Math.abs(augmented[maxRow]?.[i] ?? 0)) {
|
||||
maxRow = k;
|
||||
}
|
||||
}
|
||||
|
||||
[augmented[i], augmented[maxRow]] = [augmented[maxRow]!, augmented[i]!];
|
||||
|
||||
const pivot = augmented[i]![i];
|
||||
const pivot = augmented[i]?.[i];
|
||||
if (Math.abs(pivot ?? 0) < 1e-10) {
|
||||
throw new Error('Matrix is singular - possible multicollinearity');
|
||||
}
|
||||
|
||||
for (let j = 0; j < 2 * n; j++) {
|
||||
augmented[i]![j] = (augmented[i]![j] ?? 0) / (pivot ?? 1);
|
||||
augmented[i]![j] = (augmented[i]?.[j] ?? 0) / (pivot ?? 1);
|
||||
}
|
||||
|
||||
for (let k = 0; k < n; k++) {
|
||||
if (k !== i) {
|
||||
const factor = augmented[k]![i] ?? 0;
|
||||
const factor = augmented[k]?.[i] ?? 0;
|
||||
for (let j = 0; j < 2 * n; j++) {
|
||||
augmented[k]![j] = (augmented[k]![j] ?? 0) - factor * (augmented[i]![j] ?? 0);
|
||||
augmented[k]![j] = (augmented[k]?.[j] ?? 0) - factor * (augmented[i]?.[j] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -191,7 +191,7 @@ function performLinearRegression(X: number[][], y: number[]): RegressionResult {
|
|||
let XTXInv: number[][];
|
||||
try {
|
||||
XTXInv = invertMatrix(XTX);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
warnings.push('Multicollinearity detected: matrix inversion failed');
|
||||
throw new Error('Cannot perform regression: multicollinearity issue');
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ function performLinearRegression(X: number[][], y: number[]): RegressionResult {
|
|||
for (let i = 0; i < n; i++) {
|
||||
let predicted = intercept;
|
||||
for (let j = 0; j < numFeatures; j++) {
|
||||
predicted += (coefficients[j] ?? 0) * (X[i]![j] ?? 0);
|
||||
predicted += (coefficients[j] ?? 0) * (X[i]?.[j] ?? 0);
|
||||
}
|
||||
predictions.push(predicted);
|
||||
residuals.push((y[i] ?? 0) - predicted);
|
||||
|
|
@ -313,7 +313,7 @@ export const linearRegressionOLSTool = tool({
|
|||
// Validate all elements are numbers
|
||||
for (let i = 0; i < X.length; i++) {
|
||||
for (let j = 0; j < numFeatures; j++) {
|
||||
const val = X[i]![j];
|
||||
const val = X[i]?.[j];
|
||||
if (typeof val !== 'number' || !Number.isFinite(val)) {
|
||||
throw new Error(`X must contain only finite numbers (invalid at row ${i}, column ${j})`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ function checkLinkFormatting(lines: string[]): LintIssue[] {
|
|||
if (!line) continue;
|
||||
|
||||
// Check for bare URLs (not in links or code)
|
||||
const bareUrlMatch = line.match(/(?<![\(\[`])(https?:\/\/[^\s\)]+)(?![\)\]`])/);
|
||||
const bareUrlMatch = line.match(/(?<![([`])(https?:\/\/[^\s)]+)(?![)\]`])/);
|
||||
if (bareUrlMatch?.[1]) {
|
||||
const url = bareUrlMatch[1];
|
||||
// Make sure it's not in a code block or already a link
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
import { type Token, marked } from 'marked';
|
||||
import { marked, type Token } from 'marked';
|
||||
|
||||
/**
|
||||
* Heading information extracted from markdown
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ type OnboardingChecklistInput = {
|
|||
*/
|
||||
function validateDate(dateStr: string): boolean {
|
||||
const date = new Date(dateStr);
|
||||
return !isNaN(date.getTime());
|
||||
return !Number.isNaN(date.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -404,7 +404,7 @@ function formatChecklist(
|
|||
if (!byOwner.has(task.owner)) {
|
||||
byOwner.set(task.owner, []);
|
||||
}
|
||||
byOwner.get(task.owner)!.push(task);
|
||||
byOwner.get(task.owner)?.push(task);
|
||||
}
|
||||
|
||||
// Display tasks grouped by owner
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ function generateOpenAPIYaml(endpoints: EndpointDefinition[]): string {
|
|||
if (!pathGroups.has(endpoint.path)) {
|
||||
pathGroups.set(endpoint.path, []);
|
||||
}
|
||||
pathGroups.get(endpoint.path)!.push(endpoint);
|
||||
pathGroups.get(endpoint.path)?.push(endpoint);
|
||||
}
|
||||
|
||||
// Domain rule: openapi_format - Generate path and method entries
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ function pivotData(
|
|||
}
|
||||
|
||||
// Accumulate values for aggregation
|
||||
rowData.get(colValue)!.push(cellValue);
|
||||
rowData.get(colValue)?.push(cellValue);
|
||||
}
|
||||
|
||||
// Build pivoted array
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ function extractPurpose(content: string): string {
|
|||
const purposeMatch = content.match(
|
||||
/(?:purpose|objective|intent)[\s:]+(.+?)(?=\n\n|scope|policy|$)/is
|
||||
);
|
||||
if (purposeMatch && purposeMatch[1]) {
|
||||
if (purposeMatch?.[1]) {
|
||||
return purposeMatch[1].trim();
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ function extractPurpose(content: string): string {
|
|||
*/
|
||||
function extractScope(content: string): string {
|
||||
const scopeMatch = content.match(/scope[\s:]+(.+?)(?=\n\n|policy|procedure|$)/is);
|
||||
if (scopeMatch && scopeMatch[1]) {
|
||||
if (scopeMatch?.[1]) {
|
||||
return scopeMatch[1].trim();
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +229,7 @@ function extractScope(content: string): string {
|
|||
*/
|
||||
function extractPolicyStatement(content: string): string {
|
||||
const statementMatch = content.match(/policy[\s:]+(.+?)(?=\n\n|procedure|$)/is);
|
||||
if (statementMatch && statementMatch[1]) {
|
||||
if (statementMatch?.[1]) {
|
||||
return statementMatch[1].trim();
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ function extractProcedures(content: string): string[] {
|
|||
|
||||
// Look for numbered lists or bullet points
|
||||
const procedureSection = content.match(/procedure[s]?[\s:]+(.+?)(?=\n\n[A-Z]|$)/is);
|
||||
if (procedureSection && procedureSection[1]) {
|
||||
if (procedureSection?.[1]) {
|
||||
const text = procedureSection[1];
|
||||
const matches = text.match(/(?:^|\n)\s*(?:\d+\.|[-*])\s*(.+)/gm);
|
||||
if (matches) {
|
||||
|
|
@ -276,7 +276,7 @@ function extractResponsibilities(content: string): Record<string, string[]> {
|
|||
// Look for responsibility sections
|
||||
const respMatch = content.match(/responsibilit(?:ies|y)[\s:]+(.+?)(?=\n\n[A-Z]|$)/is);
|
||||
|
||||
if (respMatch && respMatch[1]) {
|
||||
if (respMatch?.[1]) {
|
||||
const text = respMatch[1];
|
||||
const roleMatches = text.matchAll(/([A-Za-z\s]+?)[:]\s*(.+?)(?=\n[A-Z]|$)/gs);
|
||||
|
||||
|
|
@ -295,11 +295,11 @@ function extractResponsibilities(content: string): Record<string, string[]> {
|
|||
|
||||
// Default responsibilities if none found
|
||||
if (Object.keys(responsibilities).length === 0) {
|
||||
responsibilities['Employees'] = [
|
||||
responsibilities.Employees = [
|
||||
'Comply with policy requirements',
|
||||
'Report violations or concerns',
|
||||
];
|
||||
responsibilities['Managers'] = [
|
||||
responsibilities.Managers = [
|
||||
'Ensure team compliance',
|
||||
'Address policy violations',
|
||||
'Provide guidance and support',
|
||||
|
|
@ -321,7 +321,7 @@ function extractDefinitions(content: string): Map<string, string> {
|
|||
const definitions = new Map<string, string>();
|
||||
|
||||
const defMatch = content.match(/definition[s]?[\s:]+(.+?)(?=\n\n[A-Z]|$)/is);
|
||||
if (defMatch && defMatch[1]) {
|
||||
if (defMatch?.[1]) {
|
||||
const text = defMatch[1];
|
||||
const termMatches = text.matchAll(/([A-Za-z\s]+?)[:]\s*(.+?)(?=\n|$)/g);
|
||||
|
||||
|
|
@ -344,7 +344,7 @@ function extractRelatedPolicies(content: string): string[] {
|
|||
const policies: string[] = [];
|
||||
|
||||
const relatedMatch = content.match(/related\s+polic(?:ies|y)[\s:]+(.+?)(?=\n\n|$)/is);
|
||||
if (relatedMatch && relatedMatch[1]) {
|
||||
if (relatedMatch?.[1]) {
|
||||
const text = relatedMatch[1];
|
||||
const policyMatches = text.match(/(?:^|\n)\s*(?:\d+\.|[-*])\s*(.+)/gm);
|
||||
if (policyMatches) {
|
||||
|
|
@ -360,7 +360,7 @@ function extractRelatedPolicies(content: string): string[] {
|
|||
*/
|
||||
function extractEnforcement(content: string): string {
|
||||
const enforcementMatch = content.match(/enforcement|violation[s]?[\s:]+(.+?)(?=\n\n|$)/is);
|
||||
if (enforcementMatch && enforcementMatch[1]) {
|
||||
if (enforcementMatch?.[1]) {
|
||||
return enforcementMatch[1].trim();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ function generateValueProposition(
|
|||
function formatPrice(price: number | string, billingPeriod?: string): string {
|
||||
const priceNum = typeof price === 'string' ? Number.parseFloat(price) : price;
|
||||
|
||||
if (isNaN(priceNum)) {
|
||||
if (Number.isNaN(priceNum)) {
|
||||
return String(price);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ function generateProposedSolution(opp: OpportunityData): ProposalSection {
|
|||
return {
|
||||
title: 'Proposed Solution',
|
||||
content: [
|
||||
'Comprehensive solution designed specifically for ' + opp.customerName,
|
||||
`Comprehensive solution designed specifically for ${opp.customerName}`,
|
||||
'Addresses all stated requirements and pain points',
|
||||
],
|
||||
subsections,
|
||||
|
|
|
|||
|
|
@ -74,19 +74,19 @@ function stringSimilarity(str1: string, str2: string): number {
|
|||
for (let i = 1; i <= s2.length; i++) {
|
||||
for (let j = 1; j <= s1.length; j++) {
|
||||
if (s2.charAt(i - 1) === s1.charAt(j - 1)) {
|
||||
matrix[i]![j] = matrix[i - 1]![j - 1]!;
|
||||
matrix[i]![j] = matrix[i - 1]?.[j - 1]!;
|
||||
} else {
|
||||
matrix[i]![j] = Math.min(
|
||||
matrix[i - 1]![j - 1]! + 1,
|
||||
matrix[i]![j - 1]! + 1,
|
||||
matrix[i - 1]![j]! + 1
|
||||
matrix[i - 1]?.[j - 1]! + 1,
|
||||
matrix[i]?.[j - 1]! + 1,
|
||||
matrix[i - 1]?.[j]! + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxLength = Math.max(s1.length, s2.length);
|
||||
return 1 - matrix[s2.length]![s1.length]! / maxLength;
|
||||
return 1 - matrix[s2.length]?.[s1.length]! / maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -53,12 +53,12 @@ const SECRET_PATTERNS: Array<{
|
|||
},
|
||||
{
|
||||
type: 'aws-secret-key',
|
||||
pattern: /aws(.{0,20})?['\"][0-9a-zA-Z/+]{40}['\"]/gi,
|
||||
pattern: /aws(.{0,20})?['"][0-9a-zA-Z/+]{40}['"]/gi,
|
||||
description: 'AWS Secret Access Key',
|
||||
},
|
||||
{
|
||||
type: 'aws-account-id',
|
||||
pattern: /aws(.{0,20})?['\"]?[0-9]{12}['\"]?/gi,
|
||||
pattern: /aws(.{0,20})?['"]?[0-9]{12}['"]?/gi,
|
||||
description: 'AWS Account ID',
|
||||
},
|
||||
|
||||
|
|
@ -109,12 +109,12 @@ const SECRET_PATTERNS: Array<{
|
|||
// Generic API Keys
|
||||
{
|
||||
type: 'generic-api-key',
|
||||
pattern: /api[_-]?key['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
|
||||
pattern: /api[_-]?key['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
|
||||
description: 'Generic API Key',
|
||||
},
|
||||
{
|
||||
type: 'generic-secret',
|
||||
pattern: /secret['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
|
||||
pattern: /secret['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
|
||||
description: 'Generic Secret',
|
||||
},
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ const SECRET_PATTERNS: Array<{
|
|||
// Generic Passwords in Code
|
||||
{
|
||||
type: 'hardcoded-password',
|
||||
pattern: /password['\"]?\s*[:=]\s*['\"][^'\"]{8,}['\"](?!\s*=)/gi,
|
||||
pattern: /password['"]?\s*[:=]\s*['"][^'"]{8,}['"](?!\s*=)/gi,
|
||||
description: 'Hardcoded Password',
|
||||
},
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ const SECRET_PATTERNS: Array<{
|
|||
// Generic Bearer Tokens
|
||||
{
|
||||
type: 'bearer-token',
|
||||
pattern: /bearer\s+[a-zA-Z0-9_\-\.=]{20,}/gi,
|
||||
pattern: /bearer\s+[a-zA-Z0-9_\-.=]{20,}/gi,
|
||||
description: 'Bearer Token',
|
||||
},
|
||||
|
||||
|
|
@ -237,7 +237,7 @@ async function generateFingerprint(value: string): Promise<string> {
|
|||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return `fallback-${Math.abs(hash).toString(16)}`;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// If all else fails, return a placeholder
|
||||
return `error-${value.length}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ function normalizeToIsoDate(dateStr: string | undefined): string | undefined {
|
|||
if (!dateStr) return undefined;
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return dateStr; // Return original if parsing fails
|
||||
if (Number.isNaN(date.getTime())) return dateStr; // Return original if parsing fails
|
||||
return date.toISOString();
|
||||
} catch {
|
||||
return dateStr; // Return original on error
|
||||
|
|
|
|||
|
|
@ -61,13 +61,13 @@ const SECRET_PATTERNS: Array<{
|
|||
},
|
||||
{
|
||||
type: 'aws-secret-key',
|
||||
pattern: /aws(.{0,20})?['\"][0-9a-zA-Z/+]{40}['\"]/gi,
|
||||
pattern: /aws(.{0,20})?['"][0-9a-zA-Z/+]{40}['"]/gi,
|
||||
severity: 'critical',
|
||||
description: 'AWS Secret Access Key',
|
||||
},
|
||||
{
|
||||
type: 'aws-account-id',
|
||||
pattern: /aws(.{0,20})?['\"]?[0-9]{12}['\"]?/gi,
|
||||
pattern: /aws(.{0,20})?['"]?[0-9]{12}['"]?/gi,
|
||||
severity: 'medium',
|
||||
description: 'AWS Account ID',
|
||||
},
|
||||
|
|
@ -126,13 +126,13 @@ const SECRET_PATTERNS: Array<{
|
|||
// Generic API Keys
|
||||
{
|
||||
type: 'generic-api-key',
|
||||
pattern: /api[_-]?key['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
|
||||
pattern: /api[_-]?key['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
|
||||
severity: 'high',
|
||||
description: 'Generic API Key',
|
||||
},
|
||||
{
|
||||
type: 'generic-secret',
|
||||
pattern: /secret['\"]?\s*[:=]\s*['\"]?[0-9a-zA-Z_\-]{20,}['\"]?/gi,
|
||||
pattern: /secret['"]?\s*[:=]\s*['"]?[0-9a-zA-Z_-]{20,}['"]?/gi,
|
||||
severity: 'high',
|
||||
description: 'Generic Secret',
|
||||
},
|
||||
|
|
@ -179,7 +179,7 @@ const SECRET_PATTERNS: Array<{
|
|||
// Generic Passwords in Code
|
||||
{
|
||||
type: 'hardcoded-password',
|
||||
pattern: /password['\"]?\s*[:=]\s*['\"][^'\"]{8,}['\"](?!\s*=)/gi,
|
||||
pattern: /password['"]?\s*[:=]\s*['"][^'"]{8,}['"](?!\s*=)/gi,
|
||||
severity: 'high',
|
||||
description: 'Hardcoded Password',
|
||||
},
|
||||
|
|
@ -219,7 +219,7 @@ const SECRET_PATTERNS: Array<{
|
|||
// Generic Bearer Tokens
|
||||
{
|
||||
type: 'bearer-token',
|
||||
pattern: /bearer\s+[a-zA-Z0-9_\-\.=]{20,}/gi,
|
||||
pattern: /bearer\s+[a-zA-Z0-9_\-.=]{20,}/gi,
|
||||
severity: 'high',
|
||||
description: 'Bearer Token',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -161,12 +161,12 @@ function formatPostContent(
|
|||
break;
|
||||
case 'casual':
|
||||
if (!content.includes('!') && !content.includes('?')) {
|
||||
content = content + '!';
|
||||
content = `${content}!`;
|
||||
}
|
||||
break;
|
||||
case 'friendly':
|
||||
if (!content.match(/[!?😊😃🎉]/u)) {
|
||||
content = content + ' 😊';
|
||||
content = `${content} 😊`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -177,7 +177,7 @@ function formatPostContent(
|
|||
case 'twitter':
|
||||
// Twitter prefers concise, punchy content
|
||||
if (content.length > 240) {
|
||||
content = content.substring(0, 237) + '...';
|
||||
content = `${content.substring(0, 237)}...`;
|
||||
}
|
||||
break;
|
||||
case 'linkedin':
|
||||
|
|
@ -185,7 +185,7 @@ function formatPostContent(
|
|||
if (!content.includes('\n\n') && content.length > 150) {
|
||||
const firstSentence = content.match(/^[^.!?]+[.!?]/)?.[0];
|
||||
if (firstSentence) {
|
||||
content = firstSentence + '\n\n' + content.substring(firstSentence.length);
|
||||
content = `${firstSentence}\n\n${content.substring(firstSentence.length)}`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -300,12 +300,12 @@ export const socialPostDraftTool = tool({
|
|||
|
||||
// Add CTA if not already in content
|
||||
if (cta && !formattedContent.toLowerCase().includes(cta.toLowerCase())) {
|
||||
finalPost += '\n\n' + cta;
|
||||
finalPost += `\n\n${cta}`;
|
||||
}
|
||||
|
||||
// Add hashtags at the end
|
||||
if (hashtags.length > 0) {
|
||||
finalPost += '\n\n' + hashtags.join(' ');
|
||||
finalPost += `\n\n${hashtags.join(' ')}`;
|
||||
}
|
||||
|
||||
const characterCount = finalPost.length;
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export const spritesCheckpointCreateTool = tool({
|
|||
// Also try to extract from complete message like "Checkpoint v2 created successfully"
|
||||
if (parsed.type === 'complete' && parsed.data && !checkpointId) {
|
||||
const match = parsed.data.match(/Checkpoint\s+(\S+)\s+created/);
|
||||
if (match && match[1]) {
|
||||
if (match?.[1]) {
|
||||
checkpointId = match[1];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ export const taxDeductionScanTool = tool({
|
|||
if (!categoryMap.has(normalizedCategory)) {
|
||||
categoryMap.set(normalizedCategory, []);
|
||||
}
|
||||
categoryMap.get(normalizedCategory)!.push(expense);
|
||||
categoryMap.get(normalizedCategory)?.push(expense);
|
||||
totalExpenses += expense.amount;
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +287,7 @@ export const taxDeductionScanTool = tool({
|
|||
const flaggedForReview: string[] = [];
|
||||
|
||||
for (const [category, categoryExpenses] of categoryMap.entries()) {
|
||||
const rules = DEDUCTION_RULES[category] || DEDUCTION_RULES['other']!;
|
||||
const rules = DEDUCTION_RULES[category] || DEDUCTION_RULES.other!;
|
||||
|
||||
// Check for entity-specific rules
|
||||
let deductionRate = rules.rate;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function extractTemplateVariables(template: string): string[] {
|
|||
if (match[1]) {
|
||||
const variable = match[1].trim();
|
||||
// Remove mustache directives (#, ^, /, etc.)
|
||||
const cleanVariable = variable.replace(/^[#^\/&]/, '').trim();
|
||||
const cleanVariable = variable.replace(/^[#^/&]/, '').trim();
|
||||
if (cleanVariable) {
|
||||
variables.add(cleanVariable);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,14 +76,14 @@ function chunkBySentences(
|
|||
if (overlap > 0) {
|
||||
// Calculate how many sentences to include for overlap
|
||||
let overlapText = '';
|
||||
let overlapSentences = 0;
|
||||
let _overlapSentences = 0;
|
||||
|
||||
for (let j = i - 1; j >= 0 && overlapText.length < overlap; j--) {
|
||||
const overlapSentence = sentences[j];
|
||||
if (!overlapSentence) continue;
|
||||
if (overlapText.length + overlapSentence.length <= overlap) {
|
||||
overlapText = `${overlapSentence} ${overlapText}`;
|
||||
overlapSentences++;
|
||||
_overlapSentences++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ export const textChunkTool = tool({
|
|||
if (chunks.length === 0) {
|
||||
chunks = chunkBySize(text, maxChunkSize, overlap);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Fall back to simple size-based chunking if sentence detection fails
|
||||
chunks = chunkBySize(text, maxChunkSize, overlap);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,14 +259,12 @@ function analyzeToS(tosText: string): TOSAnalysis {
|
|||
|
||||
if (complexWordPercentage > 20) {
|
||||
recommendations.push(
|
||||
'Reduce use of complex words (currently ' + complexWordPercentage.toFixed(1) + '% of text)'
|
||||
`Reduce use of complex words (currently ${complexWordPercentage.toFixed(1)}% of text)`
|
||||
);
|
||||
}
|
||||
|
||||
if (problematicSections.length > 0) {
|
||||
recommendations.push(
|
||||
'Address ' + problematicSections.length + ' identified problematic sections'
|
||||
);
|
||||
recommendations.push(`Address ${problematicSections.length} identified problematic sections`);
|
||||
}
|
||||
|
||||
if (totalWords > 5000) {
|
||||
|
|
|
|||
|
|
@ -3,73 +3,73 @@
|
|||
* This file provides metadata for the blocks validator
|
||||
*/
|
||||
import {
|
||||
// Execution tools
|
||||
executeCodeAsync,
|
||||
execute,
|
||||
run,
|
||||
runAsync,
|
||||
// Job tools
|
||||
getJob,
|
||||
listJobs,
|
||||
deleteJob,
|
||||
// Languages tools
|
||||
getLanguages,
|
||||
getShells,
|
||||
// Session tools
|
||||
createSession,
|
||||
getSession,
|
||||
listSessions,
|
||||
executeInSession,
|
||||
freezeSession,
|
||||
unfreezeSession,
|
||||
lockSession,
|
||||
unlockSession,
|
||||
createSessionSnapshot,
|
||||
restoreSession,
|
||||
deleteSession,
|
||||
cloneSnapshot,
|
||||
// Service tools
|
||||
createService,
|
||||
getService,
|
||||
listServices,
|
||||
executeInService,
|
||||
freezeService,
|
||||
unfreezeService,
|
||||
lockService,
|
||||
unlockService,
|
||||
redeployService,
|
||||
getServiceLogs,
|
||||
createServiceSnapshot,
|
||||
getServiceEnv,
|
||||
setServiceEnv,
|
||||
deleteServiceEnv,
|
||||
deleteService,
|
||||
// Session tools
|
||||
createSession,
|
||||
createSessionSnapshot,
|
||||
// Snapshot tools
|
||||
createSnapshot,
|
||||
getSnapshot,
|
||||
listSnapshots,
|
||||
lockSnapshot,
|
||||
unlockSnapshot,
|
||||
restoreSnapshot,
|
||||
cloneSnapshot,
|
||||
deleteSnapshot,
|
||||
// Image tools
|
||||
publishImage,
|
||||
getImage,
|
||||
listImages,
|
||||
lockImage,
|
||||
unlockImage,
|
||||
grantImageAccess,
|
||||
revokeImageAccess,
|
||||
transferImage,
|
||||
setImageVisibility,
|
||||
spawnFromImage,
|
||||
getImageTrustedKeys,
|
||||
deleteImage,
|
||||
deleteJob,
|
||||
deleteService,
|
||||
deleteServiceEnv,
|
||||
deleteSession,
|
||||
deleteSnapshot,
|
||||
execute,
|
||||
// Execution tools
|
||||
executeCodeAsync,
|
||||
executeInService,
|
||||
executeInSession,
|
||||
freezeService,
|
||||
freezeSession,
|
||||
getClusterStatus,
|
||||
getImage,
|
||||
getImageTrustedKeys,
|
||||
// Job tools
|
||||
getJob,
|
||||
// Languages tools
|
||||
getLanguages,
|
||||
getService,
|
||||
getServiceEnv,
|
||||
getServiceLogs,
|
||||
getSession,
|
||||
getShells,
|
||||
getSnapshot,
|
||||
getSystemStats,
|
||||
grantImageAccess,
|
||||
// System tools
|
||||
healthCheck,
|
||||
getClusterStatus,
|
||||
getSystemStats,
|
||||
listImages,
|
||||
listJobs,
|
||||
listPools,
|
||||
listServices,
|
||||
listSessions,
|
||||
listSnapshots,
|
||||
lockImage,
|
||||
lockService,
|
||||
lockSession,
|
||||
lockSnapshot,
|
||||
// Image tools
|
||||
publishImage,
|
||||
redeployService,
|
||||
restoreSession,
|
||||
restoreSnapshot,
|
||||
revokeImageAccess,
|
||||
run,
|
||||
runAsync,
|
||||
setImageVisibility,
|
||||
setServiceEnv,
|
||||
spawnFromImage,
|
||||
transferImage,
|
||||
unfreezeService,
|
||||
unfreezeSession,
|
||||
unlockImage,
|
||||
unlockService,
|
||||
unlockSession,
|
||||
unlockSnapshot,
|
||||
} from './src/index.js';
|
||||
|
||||
export const block = {
|
||||
|
|
|
|||
|
|
@ -595,7 +595,7 @@ export const execute = tool({
|
|||
stderr: result.stderr || '',
|
||||
exit_code: result.exit_code ?? 0,
|
||||
total_time_ms: result.total_time_ms || 0,
|
||||
success: result.success ?? (result.exit_code === 0),
|
||||
success: result.success ?? result.exit_code === 0,
|
||||
artifact: result.artifact,
|
||||
wasm_artifact: result.wasm_artifact,
|
||||
};
|
||||
|
|
@ -691,7 +691,7 @@ export const run = tool({
|
|||
stdout: result.stdout || '',
|
||||
stderr: result.stderr || '',
|
||||
exit_code: result.exit_code ?? 0,
|
||||
success: result.success ?? (result.exit_code === 0),
|
||||
success: result.success ?? result.exit_code === 0,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -896,8 +896,7 @@ export interface LanguagesResult {
|
|||
* List all supported programming languages.
|
||||
*/
|
||||
export const getLanguages = tool({
|
||||
description:
|
||||
'List all supported programming languages and their aliases for code execution.',
|
||||
description: 'List all supported programming languages and their aliases for code execution.',
|
||||
inputSchema: jsonSchema<Record<string, never>>({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
|
|
@ -923,8 +922,7 @@ export interface ShellsResult {
|
|||
* List all supported shells and REPLs for interactive sessions.
|
||||
*/
|
||||
export const getShells = tool({
|
||||
description:
|
||||
'List all supported shells and REPLs for interactive sessions, grouped by category.',
|
||||
description: 'List all supported shells and REPLs for interactive sessions, grouped by category.',
|
||||
inputSchema: jsonSchema<Record<string, never>>({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
|
|
@ -1019,7 +1017,11 @@ export const getSession = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GetSessionInput): Promise<SessionResult> {
|
||||
return apiRequest<SessionResult>('GET', `/sessions/${encodeURIComponent(input.session_id)}`, undefined);
|
||||
return apiRequest<SessionResult>(
|
||||
'GET',
|
||||
`/sessions/${encodeURIComponent(input.session_id)}`,
|
||||
undefined
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1103,7 +1105,8 @@ export interface SessionStateResult {
|
|||
* Freeze a session to save resources.
|
||||
*/
|
||||
export const freezeSession = tool({
|
||||
description: 'Freeze a session to save resources. Container state is preserved and can be woken later.',
|
||||
description:
|
||||
'Freeze a session to save resources. Container state is preserved and can be woken later.',
|
||||
inputSchema: jsonSchema<SessionIdInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
@ -1223,7 +1226,10 @@ export const createSessionSnapshot = tool({
|
|||
properties: {
|
||||
session_id: { type: 'string', description: 'The session ID' },
|
||||
name: { type: 'string', description: 'Optional snapshot name' },
|
||||
hot: { type: 'boolean', description: 'Create hot snapshot without stopping container. Default: false' },
|
||||
hot: {
|
||||
type: 'boolean',
|
||||
description: 'Create hot snapshot without stopping container. Default: false',
|
||||
},
|
||||
ttl: { type: 'number', description: 'Auto-delete after N seconds (optional)' },
|
||||
},
|
||||
required: ['session_id'],
|
||||
|
|
@ -1329,7 +1335,10 @@ export const createService = tool({
|
|||
properties: {
|
||||
name: { type: 'string', description: 'Service name (becomes NAME.on.unsandbox.com)' },
|
||||
bootstrap: { type: 'string', description: 'Bootstrap script content or URL' },
|
||||
bootstrap_content: { type: 'string', description: 'Bootstrap script content (alternative to bootstrap)' },
|
||||
bootstrap_content: {
|
||||
type: 'string',
|
||||
description: 'Bootstrap script content (alternative to bootstrap)',
|
||||
},
|
||||
ports: {
|
||||
type: 'array',
|
||||
items: { type: 'number' },
|
||||
|
|
@ -1384,7 +1393,11 @@ export const getService = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ServiceIdInput): Promise<ServiceResult> {
|
||||
return apiRequest<ServiceResult>('GET', `/services/${encodeURIComponent(input.service_id)}`, undefined);
|
||||
return apiRequest<ServiceResult>(
|
||||
'GET',
|
||||
`/services/${encodeURIComponent(input.service_id)}`,
|
||||
undefined
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1814,7 +1827,11 @@ export const getSnapshot = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: SnapshotIdInput): Promise<SnapshotResult> {
|
||||
return apiRequest<SnapshotResult>('GET', `/snapshots/${encodeURIComponent(input.snapshot_id)}`, undefined);
|
||||
return apiRequest<SnapshotResult>(
|
||||
'GET',
|
||||
`/snapshots/${encodeURIComponent(input.snapshot_id)}`,
|
||||
undefined
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -2066,7 +2083,11 @@ export const getImage = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ImageIdInput): Promise<ImageResult> {
|
||||
return apiRequest<ImageResult>('GET', `/images/${encodeURIComponent(input.image_id)}`, undefined);
|
||||
return apiRequest<ImageResult>(
|
||||
'GET',
|
||||
`/images/${encodeURIComponent(input.image_id)}`,
|
||||
undefined
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -2165,11 +2186,9 @@ export const grantImageAccess = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GrantImageAccessInput): Promise<ImageResult> {
|
||||
return apiRequest<ImageResult>(
|
||||
'POST',
|
||||
`/images/${encodeURIComponent(input.image_id)}/grant`,
|
||||
{ api_key: input.api_key }
|
||||
);
|
||||
return apiRequest<ImageResult>('POST', `/images/${encodeURIComponent(input.image_id)}/grant`, {
|
||||
api_key: input.api_key,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -2188,11 +2207,9 @@ export const revokeImageAccess = tool({
|
|||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GrantImageAccessInput): Promise<ImageResult> {
|
||||
return apiRequest<ImageResult>(
|
||||
'POST',
|
||||
`/images/${encodeURIComponent(input.image_id)}/revoke`,
|
||||
{ api_key: input.api_key }
|
||||
);
|
||||
return apiRequest<ImageResult>('POST', `/images/${encodeURIComponent(input.image_id)}/revoke`, {
|
||||
api_key: input.api_key,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -2348,7 +2365,8 @@ export interface DeleteImageResult {
|
|||
* Delete an image.
|
||||
*/
|
||||
export const deleteImage = tool({
|
||||
description: 'Permanently delete an image from LXD and the database. Cannot delete locked images.',
|
||||
description:
|
||||
'Permanently delete an image from LXD and the database. Cannot delete locked images.',
|
||||
inputSchema: jsonSchema<ImageIdInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function normalizeUrl(
|
|||
let url: URL;
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
throw new Error(`Invalid URL: ${urlString}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
cancelJob,
|
||||
executeCode,
|
||||
executeCodeAsync,
|
||||
getJob,
|
||||
listJobs,
|
||||
listLanguages,
|
||||
runCode,
|
||||
runCodeAsync,
|
||||
listJobs,
|
||||
getJob,
|
||||
cancelJob,
|
||||
listLanguages,
|
||||
} from '@thomasdavis/unsandbox';
|
||||
|
|
|
|||
|
|
@ -86,9 +86,7 @@ export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
|
|||
};
|
||||
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const [internalValue, setInternalValue] = useState<string[]>(
|
||||
normalizeValue(defaultValue)
|
||||
);
|
||||
const [internalValue, setInternalValue] = useState<string[]>(normalizeValue(defaultValue));
|
||||
|
||||
const value = isControlled ? normalizeValue(controlledValue) : internalValue;
|
||||
|
||||
|
|
@ -138,11 +136,7 @@ export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
|
|||
|
||||
return (
|
||||
<AccordionContext.Provider value={contextValue}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(accordionVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(accordionVariants({ variant }), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionContext.Provider>
|
||||
|
|
@ -232,7 +226,10 @@ export const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerPr
|
|||
aria-expanded={isExpanded}
|
||||
aria-controls={contentId}
|
||||
disabled={disabled}
|
||||
className={cn(accordionTriggerVariants({ disabled: disabled ? 'true' : 'false' }), className)}
|
||||
className={cn(
|
||||
accordionTriggerVariants({ disabled: disabled ? 'true' : 'false' }),
|
||||
className
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
|
|
@ -266,12 +263,13 @@ export const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps
|
|||
ref={ref}
|
||||
role="region"
|
||||
aria-hidden={!isExpanded}
|
||||
className={cn(accordionContentVariants({ expanded: isExpanded ? 'true' : 'false' }), className)}
|
||||
className={cn(
|
||||
accordionContentVariants({ expanded: isExpanded ? 'true' : 'false' }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className={accordionContentInnerVariants({})}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={accordionContentInnerVariants({})}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import { createVariants } from '../system/variants';
|
|||
* Accordion container variant definitions
|
||||
*/
|
||||
export const accordionVariants = createVariants({
|
||||
base: [
|
||||
'w-full',
|
||||
].join(' '),
|
||||
base: ['w-full'].join(' '),
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -25,9 +23,7 @@ export const accordionVariants = createVariants({
|
|||
* Accordion item variant definitions
|
||||
*/
|
||||
export const accordionItemVariants = createVariants({
|
||||
base: [
|
||||
'w-full',
|
||||
].join(' '),
|
||||
base: ['w-full'].join(' '),
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -67,8 +63,8 @@ export const accordionTriggerVariants = createVariants({
|
|||
|
||||
variants: {
|
||||
disabled: {
|
||||
'true': 'opacity-50 cursor-not-allowed hover:bg-transparent',
|
||||
'false': '',
|
||||
true: 'opacity-50 cursor-not-allowed hover:bg-transparent',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -81,16 +77,12 @@ export const accordionTriggerVariants = createVariants({
|
|||
* Accordion trigger icon variant definitions
|
||||
*/
|
||||
export const accordionTriggerIconVariants = createVariants({
|
||||
base: [
|
||||
'flex-shrink-0',
|
||||
'text-foreground-muted',
|
||||
'transition-transform duration-200',
|
||||
].join(' '),
|
||||
base: ['flex-shrink-0', 'text-foreground-muted', 'transition-transform duration-200'].join(' '),
|
||||
|
||||
variants: {
|
||||
expanded: {
|
||||
'true': 'rotate-180',
|
||||
'false': 'rotate-0',
|
||||
true: 'rotate-180',
|
||||
false: 'rotate-0',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -103,15 +95,12 @@ export const accordionTriggerIconVariants = createVariants({
|
|||
* Accordion content variant definitions
|
||||
*/
|
||||
export const accordionContentVariants = createVariants({
|
||||
base: [
|
||||
'overflow-hidden',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
].join(' '),
|
||||
base: ['overflow-hidden', 'transition-all duration-200 ease-in-out'].join(' '),
|
||||
|
||||
variants: {
|
||||
expanded: {
|
||||
'true': 'max-h-[1000px] opacity-100',
|
||||
'false': 'max-h-0 opacity-0',
|
||||
true: 'max-h-[1000px] opacity-100',
|
||||
false: 'max-h-0 opacity-0',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -124,11 +113,7 @@ export const accordionContentVariants = createVariants({
|
|||
* Accordion content inner variant definitions
|
||||
*/
|
||||
export const accordionContentInnerVariants = createVariants({
|
||||
base: [
|
||||
'px-4 pb-4',
|
||||
'font-mono text-sm',
|
||||
'text-foreground-muted',
|
||||
].join(' '),
|
||||
base: ['px-4 pb-4', 'font-mono text-sm', 'text-foreground-muted'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { cn } from '@tpmjs/utils/cn';
|
||||
import {
|
||||
Children,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Children, forwardRef, isValidElement, useMemo, useState } from 'react';
|
||||
import { Icon, type IconName } from '../Icon/Icon';
|
||||
import type {
|
||||
BreadcrumbEllipsisProps,
|
||||
|
|
@ -38,7 +32,6 @@ function getSeparatorIcon(separator: BreadcrumbSeparator): IconName {
|
|||
return 'arrowRight';
|
||||
case 'dot':
|
||||
return 'circle';
|
||||
case 'slash':
|
||||
default:
|
||||
return 'slash';
|
||||
}
|
||||
|
|
@ -103,11 +96,7 @@ export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
|
|||
</BreadcrumbSeparatorComponent>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<BreadcrumbSeparatorComponent key={key}>
|
||||
{separator}
|
||||
</BreadcrumbSeparatorComponent>
|
||||
);
|
||||
return <BreadcrumbSeparatorComponent key={key}>{separator}</BreadcrumbSeparatorComponent>;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -129,10 +118,7 @@ export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
|
|||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className={breadcrumbItemVariants({})}
|
||||
>
|
||||
<li key={index} className={breadcrumbItemVariants({})}>
|
||||
{index > 0 && renderSeparator(`sep-${index}`)}
|
||||
{item}
|
||||
</li>
|
||||
|
|
@ -261,17 +247,18 @@ BreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis';
|
|||
*
|
||||
* Represents the current page in breadcrumbs (non-link, just text)
|
||||
*/
|
||||
export const BreadcrumbPage = forwardRef<HTMLSpanElement, Omit<BreadcrumbItemProps, 'href' | 'current'>>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
aria-current="page"
|
||||
className={cn(breadcrumbLinkVariants({ current: 'true' }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
export const BreadcrumbPage = forwardRef<
|
||||
HTMLSpanElement,
|
||||
Omit<BreadcrumbItemProps, 'href' | 'current'>
|
||||
>(({ children, className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
aria-current="page"
|
||||
className={cn(breadcrumbLinkVariants({ current: 'true' }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
));
|
||||
|
||||
BreadcrumbPage.displayName = 'BreadcrumbPage';
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ import { createVariants } from '../system/variants';
|
|||
* Breadcrumbs container variant definitions
|
||||
*/
|
||||
export const breadcrumbsVariants = createVariants({
|
||||
base: [
|
||||
'flex items-center',
|
||||
'font-mono text-sm',
|
||||
].join(' '),
|
||||
base: ['flex items-center', 'font-mono text-sm'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
@ -18,12 +15,7 @@ export const breadcrumbsVariants = createVariants({
|
|||
* Breadcrumbs list variant definitions
|
||||
*/
|
||||
export const breadcrumbsListVariants = createVariants({
|
||||
base: [
|
||||
'flex items-center gap-1',
|
||||
'flex-wrap',
|
||||
'list-none',
|
||||
'm-0 p-0',
|
||||
].join(' '),
|
||||
base: ['flex items-center gap-1', 'flex-wrap', 'list-none', 'm-0 p-0'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
@ -34,14 +26,12 @@ export const breadcrumbsListVariants = createVariants({
|
|||
* Breadcrumb item variant definitions
|
||||
*/
|
||||
export const breadcrumbItemVariants = createVariants({
|
||||
base: [
|
||||
'flex items-center gap-1',
|
||||
].join(' '),
|
||||
base: ['flex items-center gap-1'].join(' '),
|
||||
|
||||
variants: {
|
||||
current: {
|
||||
'true': '',
|
||||
'false': '',
|
||||
true: '',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -65,8 +55,8 @@ export const breadcrumbLinkVariants = createVariants({
|
|||
|
||||
variants: {
|
||||
current: {
|
||||
'true': 'text-foreground font-medium pointer-events-none',
|
||||
'false': 'cursor-pointer',
|
||||
true: 'text-foreground font-medium pointer-events-none',
|
||||
false: 'cursor-pointer',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -79,12 +69,7 @@ export const breadcrumbLinkVariants = createVariants({
|
|||
* Breadcrumb separator variant definitions
|
||||
*/
|
||||
export const breadcrumbSeparatorVariants = createVariants({
|
||||
base: [
|
||||
'mx-1',
|
||||
'text-foreground-muted',
|
||||
'select-none',
|
||||
'opacity-60',
|
||||
].join(' '),
|
||||
base: ['mx-1', 'text-foreground-muted', 'select-none', 'opacity-60'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
@ -113,11 +98,7 @@ export const breadcrumbEllipsisVariants = createVariants({
|
|||
* Breadcrumb icon variant definitions
|
||||
*/
|
||||
export const breadcrumbIconVariants = createVariants({
|
||||
base: [
|
||||
'flex-shrink-0',
|
||||
'w-4 h-4',
|
||||
'mr-1',
|
||||
].join(' '),
|
||||
base: ['flex-shrink-0', 'w-4 h-4', 'mr-1'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ export const cardVariants = createVariants({
|
|||
|
||||
variants: {
|
||||
variant: {
|
||||
default: [
|
||||
'border border-dashed border-border',
|
||||
'bg-card text-card-foreground',
|
||||
].join(' '),
|
||||
default: ['border border-dashed border-border', 'bg-card text-card-foreground'].join(' '),
|
||||
|
||||
elevated: [
|
||||
'border border-dashed border-border',
|
||||
|
|
@ -28,17 +25,13 @@ export const cardVariants = createVariants({
|
|||
|
||||
outline: ['border-2 border-dashed border-border', 'bg-transparent text-foreground'].join(' '),
|
||||
|
||||
blueprint: [
|
||||
'border border-dashed border-border',
|
||||
'bg-card text-card-foreground',
|
||||
].join(' '),
|
||||
blueprint: ['border border-dashed border-border', 'bg-card text-card-foreground'].join(' '),
|
||||
|
||||
ghost: ['bg-transparent text-foreground'].join(' '),
|
||||
|
||||
featured: [
|
||||
'border-2 border-solid border-foreground',
|
||||
'bg-card text-card-foreground',
|
||||
].join(' '),
|
||||
featured: ['border-2 border-solid border-foreground', 'bg-card text-card-foreground'].join(
|
||||
' '
|
||||
),
|
||||
|
||||
brutalist: [
|
||||
'border-[6px] border-foreground',
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import { cn } from '@tpmjs/utils/cn';
|
|||
import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import type {
|
||||
DrawerBodyProps,
|
||||
DrawerFooterProps,
|
||||
DrawerHeaderProps,
|
||||
DrawerProps,
|
||||
} from './types';
|
||||
import type { DrawerBodyProps, DrawerFooterProps, DrawerHeaderProps, DrawerProps } from './types';
|
||||
import {
|
||||
drawerBackdropVariants,
|
||||
drawerBodyVariants,
|
||||
|
|
@ -135,30 +130,27 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
|
|||
);
|
||||
|
||||
// Handle focus trap within drawer
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Tab') return;
|
||||
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Tab') return;
|
||||
|
||||
const panel = panelRef.current;
|
||||
if (!panel) return;
|
||||
const panel = panelRef.current;
|
||||
if (!panel) return;
|
||||
|
||||
const focusableElements = panel.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const focusableElements = panel.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (event.shiftKey && document.activeElement === firstElement) {
|
||||
event.preventDefault();
|
||||
lastElement?.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
||||
event.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
if (event.shiftKey && document.activeElement === firstElement) {
|
||||
event.preventDefault();
|
||||
lastElement?.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
||||
event.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
|
|
@ -175,10 +167,7 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
|
|||
/>
|
||||
|
||||
{/* Container */}
|
||||
<div
|
||||
className={drawerContainerVariants({})}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className={drawerContainerVariants({})} onKeyDown={handleKeyDown}>
|
||||
{/* Panel */}
|
||||
<div
|
||||
ref={(node) => {
|
||||
|
|
@ -195,10 +184,7 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
|
|||
aria-labelledby={title ? titleId : undefined}
|
||||
aria-describedby={description ? descriptionId : undefined}
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
drawerPanelVariants({ side, size, state: 'entered' }),
|
||||
className
|
||||
)}
|
||||
className={cn(drawerPanelVariants({ side, size, state: 'entered' }), className)}
|
||||
{...props}
|
||||
>
|
||||
{/* Header */}
|
||||
|
|
@ -230,16 +216,10 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
|
|||
)}
|
||||
|
||||
{/* Body */}
|
||||
<div className={drawerBodyVariants({})}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={drawerBodyVariants({})}>{children}</div>
|
||||
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<div className={drawerFooterVariants({})}>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
{footer && <div className={drawerFooterVariants({})}>{footer}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -256,11 +236,7 @@ Drawer.displayName = 'Drawer';
|
|||
*/
|
||||
export const DrawerHeader = forwardRef<HTMLDivElement, DrawerHeaderProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(drawerHeaderVariants({}), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(drawerHeaderVariants({}), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -273,11 +249,7 @@ DrawerHeader.displayName = 'DrawerHeader';
|
|||
*/
|
||||
export const DrawerBody = forwardRef<HTMLDivElement, DrawerBodyProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(drawerBodyVariants({}), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(drawerBodyVariants({}), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -290,11 +262,7 @@ DrawerBody.displayName = 'DrawerBody';
|
|||
*/
|
||||
export const DrawerFooter = forwardRef<HTMLDivElement, DrawerFooterProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(drawerFooterVariants({}), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(drawerFooterVariants({}), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ export const drawerBackdropVariants = createVariants({
|
|||
* Drawer container variant definitions
|
||||
*/
|
||||
export const drawerContainerVariants = createVariants({
|
||||
base: [
|
||||
'fixed inset-0',
|
||||
'z-[var(--z-drawer)]',
|
||||
'overflow-hidden',
|
||||
].join(' '),
|
||||
base: ['fixed inset-0', 'z-[var(--z-drawer)]', 'overflow-hidden'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
@ -141,11 +137,7 @@ export const drawerHeaderVariants = createVariants({
|
|||
* Drawer body variant definitions
|
||||
*/
|
||||
export const drawerBodyVariants = createVariants({
|
||||
base: [
|
||||
'flex-1',
|
||||
'px-6 py-4',
|
||||
'overflow-y-auto',
|
||||
].join(' '),
|
||||
base: ['flex-1', 'px-6 py-4', 'overflow-y-auto'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
@ -173,11 +165,7 @@ export const drawerFooterVariants = createVariants({
|
|||
* Drawer title variant definitions
|
||||
*/
|
||||
export const drawerTitleVariants = createVariants({
|
||||
base: [
|
||||
'font-mono font-semibold text-lg',
|
||||
'text-foreground',
|
||||
'lowercase',
|
||||
].join(' '),
|
||||
base: ['font-mono font-semibold text-lg', 'text-foreground', 'lowercase'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
|
|||
|
|
@ -186,8 +186,8 @@ export const DropdownMenu = forwardRef<HTMLDivElement, DropdownMenuProps>(
|
|||
if (!isOpen || !triggerRef.current || !contentRef.current) return;
|
||||
|
||||
const updatePosition = () => {
|
||||
const triggerRect = triggerRef.current!.getBoundingClientRect();
|
||||
const contentRect = contentRef.current!.getBoundingClientRect();
|
||||
const triggerRect = triggerRef.current?.getBoundingClientRect();
|
||||
const contentRect = contentRef.current?.getBoundingClientRect();
|
||||
const newPosition = calculatePosition(triggerRect, contentRect, placement, offset);
|
||||
setPosition(newPosition);
|
||||
};
|
||||
|
|
@ -209,10 +209,7 @@ export const DropdownMenu = forwardRef<HTMLDivElement, DropdownMenuProps>(
|
|||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (
|
||||
triggerRef.current?.contains(target) ||
|
||||
contentRef.current?.contains(target)
|
||||
) {
|
||||
if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
|
||||
return;
|
||||
}
|
||||
closeMenu();
|
||||
|
|
@ -236,15 +233,11 @@ export const DropdownMenu = forwardRef<HTMLDivElement, DropdownMenuProps>(
|
|||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
setActiveIndex((prev) =>
|
||||
prev < itemCountRef.current - 1 ? prev + 1 : 0
|
||||
);
|
||||
setActiveIndex((prev) => (prev < itemCountRef.current - 1 ? prev + 1 : 0));
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
setActiveIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : itemCountRef.current - 1
|
||||
);
|
||||
setActiveIndex((prev) => (prev > 0 ? prev - 1 : itemCountRef.current - 1));
|
||||
break;
|
||||
case 'Home':
|
||||
event.preventDefault();
|
||||
|
|
@ -391,16 +384,16 @@ export const DropdownMenuItem = forwardRef<HTMLButtonElement, DropdownMenuItemPr
|
|||
{...props}
|
||||
>
|
||||
{icon && (
|
||||
<span className={dropdownMenuItemIconVariants({ destructive: destructive ? 'true' : 'false' })}>
|
||||
<span
|
||||
className={dropdownMenuItemIconVariants({
|
||||
destructive: destructive ? 'true' : 'false',
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1">{children}</span>
|
||||
{shortcut && (
|
||||
<span className={dropdownMenuItemShortcutVariants({})}>
|
||||
{shortcut}
|
||||
</span>
|
||||
)}
|
||||
{shortcut && <span className={dropdownMenuItemShortcutVariants({})}>{shortcut}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -429,11 +422,7 @@ DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
|
|||
*/
|
||||
export const DropdownMenuLabel = forwardRef<HTMLDivElement, DropdownMenuLabelProps>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(dropdownMenuLabelVariants({}), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(dropdownMenuLabelVariants({}), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,16 +55,16 @@ export const dropdownMenuItemVariants = createVariants({
|
|||
|
||||
variants: {
|
||||
disabled: {
|
||||
'true': 'opacity-50 cursor-not-allowed hover:bg-transparent',
|
||||
'false': '',
|
||||
true: 'opacity-50 cursor-not-allowed hover:bg-transparent',
|
||||
false: '',
|
||||
},
|
||||
destructive: {
|
||||
'true': 'text-error hover:bg-error/10 focus-visible:bg-error/10',
|
||||
'false': '',
|
||||
true: 'text-error hover:bg-error/10 focus-visible:bg-error/10',
|
||||
false: '',
|
||||
},
|
||||
active: {
|
||||
'true': 'bg-accent/10',
|
||||
'false': '',
|
||||
true: 'bg-accent/10',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -79,16 +79,12 @@ export const dropdownMenuItemVariants = createVariants({
|
|||
* Dropdown menu item icon variant definitions
|
||||
*/
|
||||
export const dropdownMenuItemIconVariants = createVariants({
|
||||
base: [
|
||||
'flex-shrink-0',
|
||||
'w-4 h-4',
|
||||
'text-foreground-muted',
|
||||
].join(' '),
|
||||
base: ['flex-shrink-0', 'w-4 h-4', 'text-foreground-muted'].join(' '),
|
||||
|
||||
variants: {
|
||||
destructive: {
|
||||
'true': 'text-error',
|
||||
'false': '',
|
||||
true: 'text-error',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -101,12 +97,7 @@ export const dropdownMenuItemIconVariants = createVariants({
|
|||
* Dropdown menu item shortcut variant definitions
|
||||
*/
|
||||
export const dropdownMenuItemShortcutVariants = createVariants({
|
||||
base: [
|
||||
'ml-auto',
|
||||
'font-mono text-xs',
|
||||
'text-foreground-muted',
|
||||
'opacity-60',
|
||||
].join(' '),
|
||||
base: ['ml-auto', 'font-mono text-xs', 'text-foreground-muted', 'opacity-60'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
@ -117,11 +108,7 @@ export const dropdownMenuItemShortcutVariants = createVariants({
|
|||
* Dropdown menu separator variant definitions
|
||||
*/
|
||||
export const dropdownMenuSeparatorVariants = createVariants({
|
||||
base: [
|
||||
'my-1',
|
||||
'h-px',
|
||||
'bg-border',
|
||||
].join(' '),
|
||||
base: ['my-1', 'h-px', 'bg-border'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
|
|||
|
|
@ -63,18 +63,11 @@ const sizeConfig = {
|
|||
* ```
|
||||
*/
|
||||
export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
|
||||
(
|
||||
{ className, icon, title, description, action, size = 'md', ...props },
|
||||
ref
|
||||
) => {
|
||||
({ className, icon, title, description, action, size = 'md', ...props }, ref) => {
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-center', config.container, className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn('text-center', config.container, className)} {...props}>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4',
|
||||
|
|
@ -83,21 +76,9 @@ export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
|
|||
>
|
||||
<Icon icon={icon} size={config.iconSize} className="text-primary" />
|
||||
</div>
|
||||
<h3
|
||||
className={cn(
|
||||
'font-medium text-foreground mb-2',
|
||||
config.title
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<h3 className={cn('font-medium text-foreground mb-2', config.title)}>{title}</h3>
|
||||
{description && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-secondary mb-4 max-w-md mx-auto',
|
||||
config.description
|
||||
)}
|
||||
>
|
||||
<p className={cn('text-foreground-secondary mb-4 max-w-md mx-auto', config.description)}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -55,36 +55,14 @@ const sizeConfig = {
|
|||
* ```
|
||||
*/
|
||||
export const ErrorState = forwardRef<HTMLDivElement, ErrorStateProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
title = 'Error',
|
||||
message,
|
||||
onRetry,
|
||||
size = 'md',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
({ className, title = 'Error', message, onRetry, size = 'md', ...props }, ref) => {
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-center', config.container, className)}
|
||||
{...props}
|
||||
>
|
||||
<Icon
|
||||
icon="alertCircle"
|
||||
size={config.iconSize}
|
||||
className="mx-auto text-error mb-4"
|
||||
/>
|
||||
<h2 className={cn('font-medium text-foreground mb-2', config.title)}>
|
||||
{title}
|
||||
</h2>
|
||||
<p className={cn('text-foreground-secondary mb-4', config.message)}>
|
||||
{message}
|
||||
</p>
|
||||
<div ref={ref} className={cn('text-center', config.container, className)} {...props}>
|
||||
<Icon icon="alertCircle" size={config.iconSize} className="mx-auto text-error mb-4" />
|
||||
<h2 className={cn('font-medium text-foreground mb-2', config.title)}>{title}</h2>
|
||||
<p className={cn('text-foreground-secondary mb-4', config.message)}>{message}</p>
|
||||
{onRetry && (
|
||||
<Button onClick={onRetry} variant="default">
|
||||
Try Again
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { createVariants } from '../system/variants';
|
||||
import {
|
||||
formErrorMessage,
|
||||
formHelperBase,
|
||||
formLabelBase,
|
||||
formRequiredIndicator,
|
||||
} from '../system/formVariants';
|
||||
import { createVariants } from '../system/variants';
|
||||
|
||||
/**
|
||||
* FormField container variants
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import type { InstallSnippetProps, PackageManager } from './types';
|
|||
import {
|
||||
installSnippetCodeVariants,
|
||||
installSnippetCopyButtonVariants,
|
||||
installSnippetTabVariants,
|
||||
installSnippetTabsVariants,
|
||||
installSnippetTabVariants,
|
||||
installSnippetVariants,
|
||||
} from './variants';
|
||||
|
||||
|
|
@ -127,11 +127,7 @@ export const InstallSnippet = forwardRef<HTMLDivElement, InstallSnippetProps>(
|
|||
}, [currentCommand]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(installSnippetVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(installSnippetVariants({ variant }), className)} {...props}>
|
||||
{/* Tabs */}
|
||||
{showTabs && (
|
||||
<div className={installSnippetTabsVariants({ variant })}>
|
||||
|
|
@ -159,7 +155,10 @@ export const InstallSnippet = forwardRef<HTMLDivElement, InstallSnippetProps>(
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className={installSnippetCopyButtonVariants({ variant, copied: copied ? 'true' : 'false' })}
|
||||
className={installSnippetCopyButtonVariants({
|
||||
variant,
|
||||
copied: copied ? 'true' : 'false',
|
||||
})}
|
||||
aria-label={copied ? 'Copied!' : 'Copy to clipboard'}
|
||||
>
|
||||
<Icon icon={copied ? 'check' : 'copy'} size="sm" />
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import { createVariants } from '../system/variants';
|
|||
* InstallSnippet container variant definitions
|
||||
*/
|
||||
export const installSnippetVariants = createVariants({
|
||||
base: [
|
||||
'w-full',
|
||||
'border border-border',
|
||||
'rounded-none',
|
||||
'overflow-hidden',
|
||||
].join(' '),
|
||||
base: ['w-full', 'border border-border', 'rounded-none', 'overflow-hidden'].join(' '),
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -28,12 +23,7 @@ export const installSnippetVariants = createVariants({
|
|||
* InstallSnippet tabs variant definitions
|
||||
*/
|
||||
export const installSnippetTabsVariants = createVariants({
|
||||
base: [
|
||||
'flex items-center',
|
||||
'border-b border-border',
|
||||
'px-1',
|
||||
'gap-0',
|
||||
].join(' '),
|
||||
base: ['flex items-center', 'border-b border-border', 'px-1', 'gap-0'].join(' '),
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -68,8 +58,8 @@ export const installSnippetTabVariants = createVariants({
|
|||
dark: 'text-background/60 hover:text-background',
|
||||
},
|
||||
active: {
|
||||
'true': '',
|
||||
'false': '',
|
||||
true: '',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -79,9 +69,18 @@ export const installSnippetTabVariants = createVariants({
|
|||
},
|
||||
|
||||
compoundVariants: [
|
||||
{ conditions: { variant: 'default', active: 'true' }, className: 'text-foreground border-primary' },
|
||||
{ conditions: { variant: 'minimal', active: 'true' }, className: 'text-foreground border-primary' },
|
||||
{ conditions: { variant: 'dark', active: 'true' }, className: 'text-background border-background' },
|
||||
{
|
||||
conditions: { variant: 'default', active: 'true' },
|
||||
className: 'text-foreground border-primary',
|
||||
},
|
||||
{
|
||||
conditions: { variant: 'minimal', active: 'true' },
|
||||
className: 'text-foreground border-primary',
|
||||
},
|
||||
{
|
||||
conditions: { variant: 'dark', active: 'true' },
|
||||
className: 'text-background border-background',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
@ -130,8 +129,8 @@ export const installSnippetCopyButtonVariants = createVariants({
|
|||
dark: 'text-background/60 hover:text-background hover:bg-background/10',
|
||||
},
|
||||
copied: {
|
||||
'true': '',
|
||||
'false': '',
|
||||
true: '',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -54,21 +54,13 @@ export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
|
|||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-center',
|
||||
config.container,
|
||||
config.gap,
|
||||
className
|
||||
)}
|
||||
className={cn('flex items-center justify-center', config.container, config.gap, className)}
|
||||
{...props}
|
||||
>
|
||||
<Spinner size={config.spinnerSize} />
|
||||
{message && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-foreground-secondary font-mono tracking-wide',
|
||||
config.messageClass
|
||||
)}
|
||||
className={cn('text-foreground-secondary font-mono tracking-wide', config.messageClass)}
|
||||
>
|
||||
{message}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import { cn } from '@tpmjs/utils/cn';
|
|||
import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import type {
|
||||
ModalBodyProps,
|
||||
ModalFooterProps,
|
||||
ModalHeaderProps,
|
||||
ModalProps,
|
||||
} from './types';
|
||||
import type { ModalBodyProps, ModalFooterProps, ModalHeaderProps, ModalProps } from './types';
|
||||
import {
|
||||
modalBackdropVariants,
|
||||
modalBodyVariants,
|
||||
|
|
@ -133,30 +128,27 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||
);
|
||||
|
||||
// Handle focus trap within modal
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Tab') return;
|
||||
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Tab') return;
|
||||
|
||||
const panel = panelRef.current;
|
||||
if (!panel) return;
|
||||
const panel = panelRef.current;
|
||||
if (!panel) return;
|
||||
|
||||
const focusableElements = panel.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const focusableElements = panel.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (event.shiftKey && document.activeElement === firstElement) {
|
||||
event.preventDefault();
|
||||
lastElement?.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
||||
event.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
if (event.shiftKey && document.activeElement === firstElement) {
|
||||
event.preventDefault();
|
||||
lastElement?.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
||||
event.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
|
|
@ -166,10 +158,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||
const modalContent = (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className={modalBackdropVariants({ state: 'entered' })}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className={modalBackdropVariants({ state: 'entered' })} aria-hidden="true" />
|
||||
|
||||
{/* Container */}
|
||||
<div
|
||||
|
|
@ -193,10 +182,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||
aria-labelledby={title ? titleId : undefined}
|
||||
aria-describedby={description ? descriptionId : undefined}
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
modalPanelVariants({ size, state: 'entered' }),
|
||||
className
|
||||
)}
|
||||
className={cn(modalPanelVariants({ size, state: 'entered' }), className)}
|
||||
{...props}
|
||||
>
|
||||
{/* Header */}
|
||||
|
|
@ -228,16 +214,10 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||
)}
|
||||
|
||||
{/* Body */}
|
||||
<div className={modalBodyVariants({})}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={modalBodyVariants({})}>{children}</div>
|
||||
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<div className={modalFooterVariants({})}>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
{footer && <div className={modalFooterVariants({})}>{footer}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -254,11 +234,7 @@ Modal.displayName = 'Modal';
|
|||
*/
|
||||
export const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(modalHeaderVariants({}), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(modalHeaderVariants({}), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -271,11 +247,7 @@ ModalHeader.displayName = 'ModalHeader';
|
|||
*/
|
||||
export const ModalBody = forwardRef<HTMLDivElement, ModalBodyProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(modalBodyVariants({}), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(modalBodyVariants({}), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -288,11 +260,7 @@ ModalBody.displayName = 'ModalBody';
|
|||
*/
|
||||
export const ModalFooter = forwardRef<HTMLDivElement, ModalFooterProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(modalFooterVariants({}), className)}
|
||||
{...props}
|
||||
>
|
||||
<div ref={ref} className={cn(modalFooterVariants({}), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -101,11 +101,7 @@ export const modalHeaderVariants = createVariants({
|
|||
* Modal body variant definitions
|
||||
*/
|
||||
export const modalBodyVariants = createVariants({
|
||||
base: [
|
||||
'flex-1',
|
||||
'px-6 py-4',
|
||||
'overflow-y-auto',
|
||||
].join(' '),
|
||||
base: ['flex-1', 'px-6 py-4', 'overflow-y-auto'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
@ -132,11 +128,7 @@ export const modalFooterVariants = createVariants({
|
|||
* Modal title variant definitions
|
||||
*/
|
||||
export const modalTitleVariants = createVariants({
|
||||
base: [
|
||||
'font-mono font-semibold text-lg',
|
||||
'text-foreground',
|
||||
'lowercase',
|
||||
].join(' '),
|
||||
base: ['font-mono font-semibold text-lg', 'text-foreground', 'lowercase'].join(' '),
|
||||
|
||||
variants: {},
|
||||
|
||||
|
|
|
|||
|
|
@ -52,28 +52,16 @@ const sizeConfig = {
|
|||
* ```
|
||||
*/
|
||||
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||
(
|
||||
{ className, title, description, actions, size = 'md', ...props },
|
||||
ref
|
||||
) => {
|
||||
({ className, title, description, actions, size = 'md', ...props }, ref) => {
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn(config.container, className)} {...props}>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h1 className={cn('font-bold text-foreground', config.title)}>
|
||||
{title}
|
||||
</h1>
|
||||
<h1 className={cn('font-bold text-foreground', config.title)}>{title}</h1>
|
||||
{description && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-secondary',
|
||||
config.description
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
<p className={cn('text-foreground-secondary', config.description)}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue