Skip to content

Commit

Permalink
multiple chat instances
Browse files Browse the repository at this point in the history
  • Loading branch information
anvaka committed May 12, 2023
1 parent ba4f818 commit 108f79f
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 111 deletions.
21 changes: 15 additions & 6 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SmallPreview from './components/SmallPreview.vue';
import About from './components/About.vue';
import UnsavedChanges from './components/UnsavedChanges.vue';
import LargestRepositories from './components/LargestRepositories.vue';
import GroupViewModel from './lib/GroupViewModel';
import bus from './lib/bus'
Expand All @@ -17,7 +18,7 @@ const smallPreviewName = ref('');
const tooltip = ref(null);
const contextMenu = ref(null);
const aboutVisible = ref(false);
const largestRepositoriesList = ref(null);
const currentGroup = ref(null);
const unsavedChangesVisible = ref(false);
const hasUnsavedChanges = ref(false);
const isSmallScreen = ref(window.innerWidth < SM_SCREEN_BREAKPOINT)
Expand All @@ -26,6 +27,8 @@ let lastSelected;
function onTypeAheadInput() {
}
const groupCache = new Map();
function closeSideBarViewer() {
sidebarVisible.value = false;
currentProject.value = '';
Expand Down Expand Up @@ -104,21 +107,27 @@ function doContextMenuAction(menuItem) {
}
function onShowLargest(largest) {
largestRepositoriesList.value = largest;
function onShowLargest(groupId, largest) {
let groupViewModel = groupCache.get(groupId);
if (!groupViewModel) {
groupViewModel = new GroupViewModel(groupId);
groupCache.set(groupId, groupViewModel);
}
groupViewModel.setLargest(largest);
currentGroup.value = groupViewModel;
}
function onUnsavedChangesDetected(hasChanges) {
hasUnsavedChanges.value = hasChanges;
}
function closeLargestRepositories() {
largestRepositoriesList.value = null
currentGroup.value = null
window.mapOwner?.clearBorderHighlights();
}
const typeAheadVisible = computed(() => {
return !(isSmallScreen.value && largestRepositoriesList.value && !currentProject.value);
return !(isSmallScreen.value && currentGroup.value && !currentProject.value);
});
function showUnsavedChanges() {
Expand All @@ -138,7 +147,7 @@ function showUnsavedChanges() {
@anvaka
</a>
</div>
<largest-repositories :repos="largestRepositoriesList" v-if="largestRepositoriesList"
<largest-repositories :repos="currentGroup" v-if="currentGroup"
class="largest-repositories"
@selected="findProject"
@close="closeLargestRepositories()"
Expand Down
8 changes: 4 additions & 4 deletions src/components/ChatContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { getOpenAIModels, getStoredOpenAIKey, storeOpenAIKey } from '../lib/open
import ChatList from './ChatList.vue';
const props = defineProps({
messages: {
type: Array,
required: false
vm: {
type: Object,
required: true
},
description: {
type: String,
Expand Down Expand Up @@ -67,7 +67,7 @@ function clearKey() {

<div v-if="loadingModels">Loading models...</div>
<div v-if="errorMessage">{{ errorMessage }}</div>
<chat-list :messages="props.messages" :models="models" v-if="models.length" />
<chat-list :vm="props.vm" :models="models" v-if="models.length" />
</div>
</template>

Expand Down
98 changes: 12 additions & 86 deletions src/components/ChatList.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
<script setup>
import { defineProps, ref, nextTick, watch} from 'vue';
import generateShortRandomId from '../lib/generateShortRandomId.js';
import { chat } from '../lib/openAIClient';
import { defineProps, ref} from 'vue';
const props = defineProps({
messages: {
type: Array,
required: false
vm: {
type: Object,
required: true
},
models: {
type: Array,
Expand All @@ -15,80 +13,9 @@ const props = defineProps({
});
const selectedModel = ref(props.models[0].id);
const error = ref('');
let loading = ref(false);
let pendingRequest;
let computedMessages = ref(props.messages?.map((message, idx) => {
return {
id: idx,
content: message.content,
role: message.role,
isEdited: false
}
}) || []);
watch(() => props.messages, (newValue, oldValue) => {
if (newValue === oldValue) return;
computedMessages.value = newValue.map((message, idx) => {
return {
id: idx,
content: message.content,
role: message.role,
isEdited: false
}
});
});
function addMessage() {
computedMessages.value.push({
id: generateShortRandomId(),
content: '',
role: 'user',
isEdited: true
});
}
function submit() {
error.value = '';
pendingRequest?.cancel();
let request = {
model: selectedModel.value,
messages: computedMessages.value.map(message => {
return {
content: message.content,
role: message.role
}
}),
}
computedMessages.value.forEach(message => {
message.isEdited = false;
});
let isCancelled = false;
loading.value = true;
let p = chat(request).then(responseMessage => {
if (isCancelled) return;
loading.value = false;
let newMessageId = generateShortRandomId();
responseMessage.id = newMessageId;
computedMessages.value.push(responseMessage);
nextTick(() => {
let newMessageEl = document.querySelector(`.add-message-link`);
if (newMessageEl) newMessageEl.scrollIntoView();
});
}).catch(err => {
console.error(err);
error.value = 'Something went wrong. Open dev console for more details';
}).finally(() => {
loading.value = false;
});
p.cancel = () => { isCancelled = true; }
pendingRequest = p;
}
function deleteMessage(id) {
computedMessages.value = computedMessages.value.filter(message => message.id !== id);
props.vm.submit(selectedModel.value);
}
function getDisplayContent(message) {
Expand All @@ -110,8 +37,7 @@ function submitOnCmdEnter(event) {
}
function cancelQuery() {
loading.value = false;
pendingRequest?.cancel();
props.vm.cancelQuery()
}
const vTextareaFitContentSize = {
Expand Down Expand Up @@ -141,7 +67,7 @@ const vTextareaFitContentSize = {
</select>
<div class="container">
<ul class="message-list">
<li v-for="message in computedMessages" :key="message.id" class="message"
<li v-for="message in vm.chat" :key="message.id" class="message"
:class="{
'user-role': message.role === 'user',
[message.id]: true
Expand All @@ -159,17 +85,17 @@ const vTextareaFitContentSize = {
v-focus
placeholder="Enter message here">
</textarea>
<a href="#" @click.prevent="deleteMessage(message.id)" class="delete" v-if="message.role !== 'system'">x</a>
<a href="#" @click.prevent="vm.deleteMessage(message.id)" class="delete" v-if="message.role !== 'system'">x</a>
</li>
<li>
<a href="#" @click.prevent="addMessage()" class="normal add-message-link" v-if="!loading">Add message</a>
<a href="#" @click.prevent="vm.addMessage()" class="normal add-message-link" v-if="!vm.loading">Add message</a>
</li>
</ul>
<div class='actions' v-if="!loading">
<div class="error" v-if="error">{{ error }}</div>
<div class='actions' v-if="!vm.loading">
<div class="error" v-if="vm.error">{{ vm.error }}</div>
<a href="#" @click.prevent="submit()" class="normal">Submit</a>
</div>
<div class="actions" v-if="loading">
<div class="actions" v-if="vm.loading">
<div class="loader-container">
<div class="loader"></div>
<a href="#" @click.prevent="cancelQuery()" class="critical label">Cancel</a>
Expand Down
17 changes: 4 additions & 13 deletions src/components/LargestRepositories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {defineProps, defineEmits, computed} from 'vue';
import ChatContainer from './ChatContainer.vue';
const props = defineProps({
repos: {
type: Array,
type: Object,
required: true
}
});
Expand All @@ -24,15 +24,6 @@ function getLink(repo) {
return 'https://github.com/' + repo.name;
}
const promptMessage = computed(() => {
return [{
role: 'system',
content: 'A user is looking at the following github repositories:' + props.repos.slice(0, 20).map(repo => '\n- ' + repo.name).join('')
}, {
role: 'user',
content: ''
}];
});
</script>
<template>
<div>
Expand All @@ -45,16 +36,16 @@ const promptMessage = computed(() => {
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
</a>In this country</h2>
<ul v-if="props.repos.length">
<li v-for="repo in props.repos" :key="repo.name">
<ul v-if="props.repos.largest.length">
<li v-for="repo in props.repos.largest" :key="repo.name">
<a :href="getLink(repo)" @click.prevent="showDetails(repo)" target="_blank">{{repo.name}}</a>
</li>
</ul>
<div v-else>
<p>No repositories found. Try zooming in?</p>
</div>
</div>
<chat-container description="Wanna learn more about these projects?" :messages="promptMessage" class="chat-container"/>
<chat-container description="Wanna learn more about these projects?" :vm="props.repos" class="chat-container"/>
</div>
</template>
<style scoped>
Expand Down
86 changes: 86 additions & 0 deletions src/lib/GroupViewModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ref, nextTick } from 'vue';
import generateShortRandomId from './generateShortRandomId';
import {sendChatRequest} from './openAIClient';

export default class GroupViewModel {
constructor() {
this.largest = ref(null);
this.chat = ref([])
this.error = ref('');
this.loading = ref(false);
this.pendingRequest = null;
}

setLargest(currentLargest) {
this.largest.value = currentLargest;
if (this.chat.value.length === 0) {
this.chat.value.push({
id: 0,
isEdited: false,
role: 'system',
content: 'A user is looking at the following github repositories:' + currentLargest.slice(0, 20).map(repo => '\n- ' + repo.name).join('')
},
{
role: 'user',
id: 1,
isEdited: false,
content: 'Please analyze these repository and detect a common theme (e.g. programming language, technology, domain). Pay attention to language too (english, chinese, korean, etc.). If there is no common theme found, please say so. Otherwise, If you can find a strong signal for a common theme please come up with a specific name for imaginary country that contains all these repositories. Give a few options. When you give an option prefer more specific over generic option (for example if repositories are about recommender systems, use that, instead of generic DeepLearning)'
});
}
}

addMessage() {
this.chat.push({
id: generateShortRandomId(),
content: '',
role: 'user',
isEdited: true
});
}

submit(model) {
this.error = '';
this.pendingRequest?.cancel();
let request = {
model,
messages: this.chat.map(message => {
return {
content: message.content,
role: message.role
}
}),
}
this.chat.forEach(message => {
message.isEdited = false;
});
let isCancelled = false;
this.loading = true;
let p = sendChatRequest(request).then(responseMessage => {
if (isCancelled) return;
this.loading = false;
let newMessageId = generateShortRandomId();
responseMessage.id = newMessageId;
this.chat.push(responseMessage);
nextTick(() => {
let newMessageEl = document.querySelector(`.add-message-link`);
if (newMessageEl) newMessageEl.scrollIntoView();
});
}).catch(err => {
console.error(err);
this.error = 'Something went wrong. Open dev console for more details';
}).finally(() => {
this.loading = false;
});
p.cancel = () => { isCancelled = true; }
this.pendingRequest = p;
}

deleteMessage(id) {
this.chat = this.chat.filter(message => message.id !== id);
}

cancelQuery() {
this.loading = false;
this.pendingRequest?.cancel();
}
}
3 changes: 2 additions & 1 deletion src/lib/createMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export default function createMap() {

map.setFilter("border-highlight", ["==", ["id"], bg.id]);
map.setLayoutProperty("border-highlight", "visibility", "visible");
bus.fire("show-largest", Array.from(seen.values()));
// todo: fire a view model here instead of the list.
bus.fire("show-largest", bg.id, Array.from(seen.values()));
}
});

Expand Down
5 changes: 4 additions & 1 deletion src/lib/openAIClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ export async function getOpenAIModels() {
});
}

export async function chat(messages) {
export async function sendChatRequest(messages) {
const headers = getAuthHeaders();
headers['Content-Type'] = 'application/json';
const body = JSON.stringify(messages);

const url = "https://api.openai.com/v1/chat/completions";
const response = await fetch(url, { method: "POST", headers, body });
const data = await response.json();
console.log(messages.messages[0].content.slice(0, 120));
console.log(data.choices[0].message.content);
console.log('-----------')

if (data?.error?.message) throw new Error(data.error.message);

Expand Down

0 comments on commit 108f79f

Please sign in to comment.