Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 29 additions & 55 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import React from 'react'
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
import { useLoading, Oval } from '@agney/react-loading'

import InputArea from './InputArea'
import IconButton from './IconButton'
import ConversationSettings from './ConversationSettings'

import type { Conversation } from '../services/conversation'

import { memo } from "react";
import React from 'react';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { useLoading, Oval } from '@agney/react-loading';
import InputArea from './InputArea';
import IconButton from './IconButton';
import ConversationSettings from './ConversationSettings';
import type { Conversation } from '../services/conversation';
export interface ChatInputProps {
prompt: string
onPromptChange: React.Dispatch<React.SetStateAction<string>>
onPromptSubmit: (event: React.FormEvent) => void
cancelPromptSubmit: (event: React.FormEvent) => void
conversation: Conversation | null
preamble?: string
onPreambleChange?: React.Dispatch<React.SetStateAction<string>>
busy?: boolean
prompt: string;
onPromptChange: React.Dispatch<React.SetStateAction<string>>;
onPromptSubmit: (event: React.FormEvent) => void;
cancelPromptSubmit: (event: React.FormEvent) => void;
conversation: Conversation | null;
preamble?: string;
onPreambleChange?: React.Dispatch<React.SetStateAction<string>>;
busy?: boolean;
}

// create a chat interface that sends user input to the openai api via the openai package
// and displays the response from openai
const ChatInput = ({
const ChatInput = memo(({
onPromptChange,
onPreambleChange,
onPromptSubmit,
Expand All @@ -31,17 +29,14 @@ const ChatInput = ({
busy = false,
conversation
}: ChatInputProps): React.ReactElement => {
const { containerProps, indicatorEl } = useLoading({
const {
containerProps,
indicatorEl
} = useLoading({
loading: busy,
indicator: <Oval width="20" />
})

return (
<form
className="ai-research-assistant__chat-form"
onSubmit={onPromptSubmit}
autoCapitalize="off"
noValidate>
});
return <form className="ai-research-assistant__chat-form" onSubmit={onPromptSubmit} autoCapitalize="off" noValidate>
<Tabs className="ai-research-assistant__chat-form__tabs" defaultIndex={0}>
<TabList>
<Tab>Prompt</Tab>
Expand All @@ -50,39 +45,18 @@ const ChatInput = ({
</TabList>

<TabPanel>
<InputArea
value={prompt}
onChange={onPromptChange}
countType="tokens"
countPosition="top"
countAlign="right"
/>
<IconButton
iconName={busy ? 'ban' : 'send'}
a11yText={busy ? 'Cancel' : 'Send'}
buttonStyle={busy ? 'danger' : 'primary'}
type={busy ? 'button' : 'submit'}
onClick={busy ? cancelPromptSubmit : () => {}}
className="ai-research-assistant__chat__input__send"
{...containerProps}>
<InputArea value={prompt} onChange={onPromptChange} countType="tokens" countPosition="top" countAlign="right" />
<IconButton iconName={busy ? 'ban' : 'send'} a11yText={busy ? 'Cancel' : 'Send'} buttonStyle={busy ? 'danger' : 'primary'} type={busy ? 'button' : 'submit'} onClick={busy ? cancelPromptSubmit : () => {}} className="ai-research-assistant__chat__input__send" {...containerProps}>
{indicatorEl}
</IconButton>
</TabPanel>
<TabPanel>
<InputArea
value={preamble}
onChange={onPreambleChange}
countType="tokens"
countPosition="top"
countAlign="right"
/>
<InputArea value={preamble} onChange={onPreambleChange} countType="tokens" countPosition="top" countAlign="right" />
</TabPanel>
<TabPanel>
<ConversationSettings conversation={conversation} />
</TabPanel>
</Tabs>
</form>
)
}

export default ChatInput
</form>;
});
export default ChatInput;
178 changes: 62 additions & 116 deletions src/components/ChatTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1,81 @@
import React, { useEffect, useState, useRef } from 'react'
import { Notice } from 'obsidian'

import IconButton from './IconButton'

import { useApp } from '../hooks/useApp'

import { DEFAULT_CONVERSATION_TITLE } from '../constants'

import React, { useEffect, useState, useRef } from 'react';
import { Notice } from 'obsidian';
import IconButton from './IconButton';
import { useApp } from '../hooks/useApp';
import { DEFAULT_CONVERSATION_TITLE } from '../constants';
export interface ChatTitleProps {
loading?: boolean
loading?: boolean;
}

const ChatTitle = ({ loading = false }: ChatTitleProps): React.ReactElement => {
const { plugin } = useApp()

const { chat, logger, settings } = plugin

const [title, setTitle] = useState(DEFAULT_CONVERSATION_TITLE)
const [updatedTitle, setUpdatedTitle] = useState('')

const [editing, setEditing] = useState(false)

const inputRef = useRef<HTMLInputElement>(null)

const ChatTitle = ({
loading = false
}: ChatTitleProps): React.ReactElement => {
const {
plugin
} = useApp();
const {
chat,
logger,
settings
} = plugin;
const [title, setTitle] = useState(DEFAULT_CONVERSATION_TITLE);
const updatedTitle = useRef('');
const [editing, setEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const onEdit = (event: React.FormEvent): void => {
event.preventDefault()

setUpdatedTitle(title)

plugin.pauseAutosaving = true

setEditing(true)
}

event.preventDefault();
updatedTitle.current = title;
plugin.pauseAutosaving = true;
setEditing(true);
};
const onSave = (event: React.FormEvent): void => {
event.preventDefault()

if (typeof updatedTitle === 'string' && updatedTitle !== '') {
plugin
.checkForExistingFile(updatedTitle)
.then((isTitleAlreadyUsed) => {
if (!isTitleAlreadyUsed) {
setTitle(updatedTitle)

chat?.currentConversation()?.updateTitle(updatedTitle)

setEditing(false)

plugin.pauseAutosaving = false
} else {
// eslint-disable-next-line no-new
new Notice(
`${updatedTitle} already exists in ${settings.conversationHistoryDirectory}`
)
}
})
.catch((error) => {
logger.error(error)
})
event.preventDefault();
if (typeof updatedTitle.current === 'string' && updatedTitle.current !== '') {
plugin.checkForExistingFile(updatedTitle.current).then(isTitleAlreadyUsed => {
if (!isTitleAlreadyUsed) {
setTitle(updatedTitle.current);
chat?.currentConversation()?.updateTitle(updatedTitle.current);
setEditing(false);
plugin.pauseAutosaving = false;
} else {
// eslint-disable-next-line no-new
new Notice(`${updatedTitle.current} already exists in ${settings.conversationHistoryDirectory}`);
}
}).catch(error => {
logger.error(error);
});
}
}

};
const onChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setUpdatedTitle(event.currentTarget.value)
}

updatedTitle.current = event.currentTarget.value;
};
useEffect(() => {
if (typeof title === 'string' && title !== '') {
chat?.currentConversation()?.updateTitle(title)
chat?.currentConversation()?.updateTitle(title);
}
}, [title])

}, [title]);
useEffect(() => {
if (
editing &&
settings.autosaveConversationHistory &&
inputRef.current !== null
) {
inputRef.current.focus()
if (editing && settings.autosaveConversationHistory && inputRef.current !== null) {
inputRef.current.focus();
}
}, [editing, inputRef.current, settings.autosaveConversationHistory])

}, [editing, inputRef.current, settings.autosaveConversationHistory]);
useEffect(() => {
if (settings.autosaveConversationHistory) {
plugin.pauseAutosaving = true

setEditing(true)
plugin.pauseAutosaving = true;
setEditing(true);
}
}, [settings.autosaveConversationHistory])

return (
<form
className="ai-research-assistant__conversation__header"
onSubmit={editing ? onSave : onEdit}>
{!editing ? (
<div className="ai-research-assistant__conversation__header__title">
}, [settings.autosaveConversationHistory]);
return <form className="ai-research-assistant__conversation__header" onSubmit={editing ? onSave : onEdit}>
{!editing ? <div className="ai-research-assistant__conversation__header__title">
{title}
</div>
) : (
<>
<label
htmlFor=""
className="ai-research-assistant__conversation__header__title__label">
</div> : <>
<label htmlFor="" className="ai-research-assistant__conversation__header__title__label">
Title
</label>
<input
type="text"
id="ai-research-assistant__conversation__title"
name="ai-research-assistant__conversation__title"
defaultValue={
settings.autosaveConversationHistory &&
title === DEFAULT_CONVERSATION_TITLE
? ''
: title
}
onChange={onChange}
ref={inputRef}
/>
</>
)}
<input type="text" id="ai-research-assistant__conversation__title" name="ai-research-assistant__conversation__title" defaultValue={settings.autosaveConversationHistory && title === DEFAULT_CONVERSATION_TITLE ? '' : title} onChange={onChange} ref={inputRef} />
</>}
<div className="ai-research-assistant__conversation__header__edit">
<IconButton
iconName={editing ? 'save' : 'pencil'}
a11yText={`${editing ? 'Save' : 'Edit'} Conversation Title`}
buttonVariant="iconOnly"
buttonStyle="primary"
type={'submit'}
/>
<IconButton iconName={editing ? 'save' : 'pencil'} a11yText={`${editing ? 'Save' : 'Edit'} Conversation Title`} buttonVariant="iconOnly" buttonStyle="primary" type={'submit'} />
</div>
</form>
)
}

export default ChatTitle
</form>;
};
export default ChatTitle;
Loading