Skip to content

Commit

Permalink
feat: Enhance chat application with file selection and session manage…
Browse files Browse the repository at this point in the history
…ment

- Updated `App.vue` to track the last answer received and pass it to `ChatHistory`.
- Integrated `dayjs` for date manipulation.
- Added `FileSelector` component to `ChatInputForm.vue` for improved file handling.
- Created `FileSelector.vue` to encapsulate file selection logic and UI.
- Introduced `SessionList.vue` to manage and display chat sessions in a reversed order.
- Moved session list logic from `ChatSessions.vue` to `SessionList.vue` for better separation of concerns.
- Updated components to use new props and event handling for file and session management.
- Refactored methods in `ChatHistory.vue` to update chat history based on `lastAnswerReceivedTime`.
  • Loading branch information
takanotume24 committed Dec 3, 2024
1 parent 3ff534b commit 1b684c6
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 75 deletions.
8 changes: 7 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<main class="col-9 d-flex flex-column p-3">
<ApiKeyDialog v-if="showDialog" @api-key-set="onApiKeySaved" />
<header class="flex-grow-1 overflow-auto mb-3">
<ChatHistory :currentSessionId="currentSessionId" />
<ChatHistory :currentSessionId="currentSessionId" :lastAnswerReceivedTime="lastAnswerReceivedTime" />
</header>
<footer class="mt-auto">
<ChatInputForm :onSubmit="handleSubmit" />
Expand All @@ -27,6 +27,7 @@ import { getDatabaseChatEntryList } from './getDatabaseChatEntryList';
import { getChatGptResponse } from './getChatGptResponse';
import { SessionId, UserInput, ModelName, ApiKey } from './types';
import { EncodedImage } from './types';
import dayjs from 'dayjs';
interface ComponentData {
input: string;
Expand All @@ -35,6 +36,7 @@ interface ComponentData {
apiKeyInput: string;
isApiKeySet: boolean;
showDialog: boolean;
lastAnswerReceivedTime: dayjs.Dayjs | null;
}
export default defineComponent({
Expand All @@ -52,6 +54,7 @@ export default defineComponent({
apiKeyInput: '',
isApiKeySet: false,
showDialog: true,
lastAnswerReceivedTime: null,
};
},
Expand All @@ -74,6 +77,7 @@ export default defineComponent({
methods: {
async handleSubmit(input: string, EncodedImageList?: EncodedImage[]) {
if (input.trim() === '') return;
const api_key = await getApiKey();
if (!api_key) return;
Expand All @@ -91,6 +95,8 @@ export default defineComponent({
);
if (!res) return;
this.lastAnswerReceivedTime = res.created_at
this.$nextTick(() => {
this.scrollToBottom();
});
Expand Down
24 changes: 19 additions & 5 deletions src/components/ChatHistory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ import { defineComponent, PropType } from 'vue';
import { SessionId } from '../types';
import { getDatabaseChatEntryBySession } from '../getDatabaseChatEntryBySession';
import { DatabaseChatEntry } from '../types';
import dayjs from 'dayjs';
export default defineComponent({
props: {
currentSessionId: {
type: [String, null] as PropType<SessionId | null>,
default: null
}
},
lastAnswerReceivedTime: {
type: [Object, null] as PropType<dayjs.Dayjs | null>,
default: null
},
},
data() {
return {
Expand All @@ -30,9 +35,11 @@ export default defineComponent({
},
watch: {
async currentSessionId(newVal: SessionId, _: SessionId) {
const databaseChatEntryBySession = await getDatabaseChatEntryBySession(newVal);
if (!databaseChatEntryBySession) return
this.databaseChatEntryBySession = databaseChatEntryBySession
this.updateChatHistory(newVal);
},
lastAnswerReceivedTime(_, __) {
if (!this.currentSessionId) return;
this.updateChatHistory(this.currentSessionId);
}
},
computed: {
Expand All @@ -42,6 +49,13 @@ export default defineComponent({
return a.created_at.diff(b.created_at);
});
}
}
},
methods: {
async updateChatHistory(session_id: SessionId) {
const databaseChatEntryBySession = await getDatabaseChatEntryBySession(session_id);
if (!databaseChatEntryBySession) return;
this.databaseChatEntryBySession = databaseChatEntryBySession
}
},
});
</script>
81 changes: 26 additions & 55 deletions src/components/ChatInputForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,89 +5,60 @@
@keydown="checkCtrlEnter"></textarea>
</div>
<div class="d-flex justify-content-between align-items-center">
<!-- Hidden default file input -->
<input type="file" @change="handleFileChange" accept="image/*" multiple class="form-control-file" ref="fileInput"
style="display: none" />
<!-- Custom button for file selection -->
<button type="button" class="btn btn-secondary" @click="triggerFileInput">
Select Files
</button>
<FileSelector @files-selected="handleFilesSelected" />
<button type="submit" class="btn btn-primary">Send</button>
</div>
<div v-if="fileNames.length" class="mt-2">
<strong>Selected files:</strong>
<ul>
<li v-for="fileName in fileNames" :key="fileName">{{ fileName }}</li>
</ul>
</div>
</form>
</template>


<script lang="ts">
import { defineComponent, ref, PropType } from 'vue';
import { defineComponent, PropType } from 'vue';
import { convertFileToBase64 } from '../convertFileToBase64';
import { EncodedImage } from '../types';
import FileSelector from './FileSelector.vue';
export default defineComponent({
components: { FileSelector },
props: {
onSubmit: Function as PropType<(input: string, base64Images?: EncodedImage[]) => void>,
},
setup(props) {
const input = ref('');
const selectedFiles = ref<File[]>([]);
const fileNames = ref<string[]>([]);
const fileInput = ref<HTMLInputElement | null>(null);
const handleSubmit = async () => {
if (props.onSubmit && input.value.trim() !== '') {
data() {
return {
input: '',
selectedFiles: [] as File[],
};
},
methods: {
async handleSubmit() {
if (this.onSubmit && this.input.trim() !== '') {
const base64Images: EncodedImage[] = [];
for (const file of selectedFiles.value) {
for (const file of this.selectedFiles) {
try {
const base64Image = await convertFileToBase64(file);
base64Images.push(base64Image);
} catch (error) {
console.error(`Error converting file ${file.name}:`, error);
}
}
props.onSubmit(input.value, base64Images);
input.value = '';
selectedFiles.value = [];
fileNames.value = [];
this.onSubmit(this.input, base64Images);
this.input = '';
this.selectedFiles = [];
}
};
const checkCtrlEnter = (event: KeyboardEvent) => {
},
checkCtrlEnter(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
handleSubmit();
}
};
const handleFileChange = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files) {
selectedFiles.value = Array.from(target.files);
fileNames.value = selectedFiles.value.map(file => file.name);
this.handleSubmit();
}
};
const triggerFileInput = () => {
fileInput.value?.click();
};
},
handleFilesSelected(files: File[]) {
this.selectedFiles = files;
},
},
mounted() {
return {
input,
fileNames,
handleSubmit,
checkCtrlEnter,
handleFileChange,
triggerFileInput,
fileInput,
};
},
});
</script>


<style scoped></style>
22 changes: 8 additions & 14 deletions src/components/ChatSessions.vue
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
<template>
<aside id="chat-sessions" class="d-flex flex-column bg-light p-3">
<NewSessionButton @new-session="createNewSession" />
<ModelSelector v -if=" isApiKeySet" :isApiKeySet="isApiKeySet" :selectedModel="selectedModel"
<ModelSelector v-if="isApiKeySet" :isApiKeySet="isApiKeySet" :selectedModel="selectedModel"
@update:selectedModel="handleModelChange" class="mb-3" />
<ul class="list-group list-group-flush flex-grow-1 overflow-auto mb-3">
<li v-for="sessionId in sessionIdList" :key="sessionId"
:class="['list-group-item', 'cursor-pointer', { active: currentSessionId === sessionId }]"
@click="selectSession(sessionId)">
Chat Session: {{ sessionId }}
</li>
</ul>
<SessionList :sessionIdList="sessionIdList" :currentSessionId="localCurrentSessionId"
@select-session="selectSession" />
</aside>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';
import ModelSelector from './ModelSelector.vue';
import NewSessionButton from './NewSessionButton.vue';
import SessionList from './SessionList.vue';
import { ModelName, SessionId } from '../types';
import { getSessionIdList } from '../getSessionIdList';
import { generateSessionId } from '../generateSessionId';
export default defineComponent({
components: { ModelSelector, NewSessionButton },
components: { ModelSelector, NewSessionButton, SessionList },
props: {
currentSessionId: {
type: [String, null] as PropType<SessionId | null>,
default: null
default: null,
},
isApiKeySet: Boolean,
selectedModel: {
type: [String, null] as PropType<ModelName | null>,
default: null
default: null,
},
},
data() {
Expand All @@ -58,7 +54,6 @@ export default defineComponent({
},
async fetchSessionIdList() {
try {
// Call your getSessionIdList function
const sessionIdList = await getSessionIdList();
if (!sessionIdList) return;
this.sessionIdList = sessionIdList;
Expand All @@ -70,8 +65,7 @@ export default defineComponent({
const newSessionId = await generateSessionId();
if (!newSessionId) return;
this.sessionIdList.push(newSessionId)
console.log(this.sessionIdList)
this.sessionIdList.push(newSessionId);
this.$emit('update:currentSessionId', newSessionId);
},
},
Expand Down
52 changes: 52 additions & 0 deletions src/components/FileSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<div>
<!-- Hidden default file input -->
<input type="file" @change="handleFileChange" accept="image/*" multiple class="form-control-file"
ref="fileInput" style="display: none" />
<!-- Custom button for file selection -->
<button type="button" class="btn btn-secondary" @click="triggerFileInput">
Select Files
</button>
<div v-if="fileNames.length" class="mt-2">
<strong>Selected files:</strong>
<ul>
<li v-for="fileName in fileNames" :key="fileName">{{ fileName }}</li>
</ul>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
emits: ['files-selected'],
setup(_, { emit }) {
const selectedFiles = ref<File[]>([]);
const fileNames = ref<string[]>([]);
const fileInput = ref<HTMLInputElement | null>(null);
const handleFileChange = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files) {
selectedFiles.value = Array.from(target.files);
fileNames.value = selectedFiles.value.map(file => file.name);
emit('files-selected', selectedFiles.value);
}
};
const triggerFileInput = () => {
fileInput.value?.click();
};
return {
fileNames,
handleFileChange,
triggerFileInput,
fileInput,
};
},
});
</script>

<style scoped></style>
39 changes: 39 additions & 0 deletions src/components/SessionList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<ul class="list-group list-group-flush flex-grow-1 overflow-auto mb-3">
<li v-for="sessionId in reversedSessionIdList" :key="sessionId"
:class="['list-group-item', 'cursor-pointer', { active: currentSessionId === sessionId }]"
@click="selectSession(sessionId)">
Chat Session: {{ sessionId }}
</li>
</ul>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { SessionId } from '../types';
export default defineComponent({
props: {
sessionIdList: {
type: Array as PropType<SessionId[]>,
required: true,
},
currentSessionId: {
type: [String, null] as PropType<SessionId | null>,
default: null,
},
},
computed: {
reversedSessionIdList(): SessionId[] {
return [...this.sessionIdList].reverse();
},
},
methods: {
selectSession(sessionId: SessionId) {
this.$emit('select-session', sessionId);
},
},
});
</script>

<style scoped></style>

0 comments on commit 1b684c6

Please sign in to comment.