Skip to content

Commit

Permalink
refactor: remove ink (#64)
Browse files Browse the repository at this point in the history
* feat: remove Ink from chat

* restore init with ink
  • Loading branch information
mdjastrzebski authored Dec 6, 2024
1 parent 97ab170 commit ab21baa
Show file tree
Hide file tree
Showing 29 changed files with 569 additions and 840 deletions.
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,19 @@
"date-fns": "^3.6.0",
"dotenv": "^16.4.5",
"ink": "^4.4.1",
"ink-link": "^3.0.0",
"ink-text-input": "^5.0.1",
"prompts": "^2.4.2",
"ora": "^8.1.1",
"react": "^18.3.1",
"redent": "^4.0.0",
"tiktoken": "^1.0.17",
"update-notifier": "^7.3.1",
"yargs": "^17.7.2",
"zod": "^3.23.8",
"zustand": "^4.5.5"
"zod": "^3.23.8"
},
"devDependencies": {
"@callstack/eslint-config": "^14.2.0",
"@release-it/conventional-changelog": "^5.1.1",
"@rslib/core": "^0.0.16",
"@types/jest": "^29.5.14",
"@types/mock-fs": "^4.13.4",
"@types/prompts": "^2.4.9",
"@types/react": "^18.3.12",
"@types/update-notifier": "^6.0.8",
"@vitest/coverage-v8": "^2.1.4",
Expand Down
52 changes: 0 additions & 52 deletions src/commands/chat/chat-commands.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Options } from 'yargs';
import { providerOptions } from './providers.js';

export interface PromptOptions {
export interface CliOptions {
/** AI inference provider to be used */
provider?: string;
/** AI model to be used */
Expand All @@ -18,7 +18,7 @@ export interface PromptOptions {
stream?: boolean;
}

export const promptOptions: Record<keyof PromptOptions, Options> = {
export const cliOptions: Record<keyof CliOptions, Options> = {
'provider': {
alias: 'p',
type: 'string',
Expand Down
91 changes: 91 additions & 0 deletions src/commands/chat/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { CHATS_SAVE_DIRECTORY } from '../../file-utils.js';
import {
exit,
getVerbose,
output,
outputVerbose,
outputWarning,
setVerbose,
} from '../../output/index.js';
import { getProvider, getProviderConfig } from './providers.js';
import { messages } from './state.js';
import { saveConversation } from './utils.js';

export function processChatCommand(input: string) {
if (!input.startsWith('/')) {
return false;
}

const command = input.split(' ')[0];
if (command === '/exit') {
exit();
}

if (command === '/help') {
outputHelp();
return true;
}

if (command === '/info') {
outputInfo();
return true;
}

if (command === '/forget') {
// Clear all messages
messages.length = 0;
return true;
}

if (command === '/verbose') {
setVerbose(!getVerbose());
output(`Verbose mode: ${getVerbose() ? 'on' : 'off'}`);
return true;
}

if (input === '/save') {
const saveConversationMessage = saveConversation(messages);
output(saveConversationMessage);
return true;
}

outputWarning(`Unknown command: ${command}`);
return true;
}

export function outputHelp() {
const lines = [
'',
'Available commands:',
' - /exit: Exit the CLI',
' - /info: Show current provider, model, and system prompt',
' - /forget: AI will forget previous messages',
` - /save: Save in a text file in ${CHATS_SAVE_DIRECTORY}`,
' - /verbose: Toggle verbose output',
'',
];

output(lines.join('\n'));
}

export function outputInfo() {
const provider = getProvider();
const providerConfig = getProviderConfig();

const lines = [
'',
'Info:',
` - Provider: ${provider.label}`,
` - Model: ${providerConfig.model}`,
` - System prompt: ${providerConfig.systemPrompt}`,
'',
];
output(lines.join('\n'));

const rawMessages = JSON.stringify(
messages.map((m) => `${m.role}: ${m.content}`),
null,
2,
);
outputVerbose(`Messages: ${rawMessages}\n`);
}
20 changes: 20 additions & 0 deletions src/commands/chat/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AssistantResponse } from '@callstack/byorg-core';
import { formatSpeed, formatTime } from '../../format.js';
import { colorAssistant, colorVerbose } from '../../output/colors.js';
import { getVerbose } from '../../output/index.js';

export function formatResponse(response: AssistantResponse) {
let result = colorAssistant(response.content);
if (getVerbose()) {
result += ` ${colorVerbose(formatResponseStats(response))}`;
}

return result;
}

function formatResponseStats(message: AssistantResponse) {
return `${formatTime(message.usage.responseTime)} ${formatSpeed(
message.usage?.outputTokens,
message.usage.responseTime,
)}`;
}
102 changes: 84 additions & 18 deletions src/commands/chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,98 @@
import { Application, createApp, Message } from '@callstack/byorg-core';
import type { CommandModule } from 'yargs';
import * as React from 'react';
import { render } from 'ink';
import { ThemeProvider } from '@inkjs/ui';
import { parseConfigFile } from '../../config-file.js';
import * as output from '../../output.js';
import { inkTheme } from '../../theme/ink-theme.js';
import { initChatState } from './state/init.js';
import { promptOptions, type PromptOptions } from './prompt-options.js';
import { ChatUi } from './ui/ChatUi.js';
import { colorAssistant } from '../../output/colors.js';
import {
clearCurrentLine,
output,
outputError,
readUserInput,
setInterruptHandler,
setVerbose,
} from '../../output/index.js';
import { processChatCommand } from './commands.js';
import { cliOptions, type CliOptions } from './cli-options.js';
import { formatResponse } from './format.js';
import { getProvider, getProviderConfig, initProvider } from './providers.js';
import { spinnerStart, spinnerStop, spinnerUpdate } from './spinner.js';
import { messages } from './state.js';
import { texts } from './texts.js';

export const command: CommandModule<{}, PromptOptions> = {
export const command: CommandModule<{}, CliOptions> = {
command: ['chat', '$0'],
describe: 'Start a conversation with AI assistant.',
builder: promptOptions,
builder: cliOptions,
handler: (args) => run(args._.join(' '), args),
};

function run(initialPrompt: string, options: PromptOptions) {
let shouldExit = false;

async function run(initialPrompt: string, options: CliOptions) {
setVerbose(options.verbose ?? false);

try {
const configFile = parseConfigFile();
initChatState(options, configFile, initialPrompt);
render(
<ThemeProvider theme={inkTheme}>
<ChatUi />
</ThemeProvider>,
);
initProvider(options, configFile);
const app = createApp({
chatModel: getProvider().getChatModel(getProviderConfig()),
systemPrompt: () => getProviderConfig().systemPrompt,
});

if (initialPrompt) {
messages.push({ role: 'user', content: initialPrompt });
}

setInterruptHandler(() => {
spinnerStop();
clearCurrentLine();
console.log('\nBye...');
shouldExit = true;
});

if (messages.length > 0) {
messages.forEach((m) => outputMessage(m));
await processMessages(app, messages);
}

while (!shouldExit) {
const userMessage = await readUserInput();
if (processChatCommand(userMessage)) {
continue;
}

messages.push({ role: 'user', content: userMessage });
await processMessages(app, messages);
}
} catch (error) {
output.outputError(error);
outputError(error);
process.exit(1);
}
}

async function processMessages(app: Application, messages: Message[]) {
const stream = getProviderConfig().stream;

const onPartialUpdate = (content: string) => {
spinnerUpdate(colorAssistant(`${texts.assistantLabel} ${content}`));
};

spinnerStart(colorAssistant(texts.assistantLabel));
const { response } = await app.processMessages(messages, {
onPartialResponse: stream ? onPartialUpdate : undefined,
});

if (response.role === 'assistant') {
messages.push({ role: 'assistant', content: response.content });
spinnerStop(`${formatResponse(response)}\n`);
} else {
spinnerStop(response.content);
}
}

function outputMessage(message: Message) {
if (message.role === 'user') {
output(`${texts.userLabel} ${message.content}`);
} else {
output(colorAssistant(`${texts.assistantLabel} ${message.content}`));
}
}
Loading

0 comments on commit ab21baa

Please sign in to comment.