Premier commit
This commit is contained in:
4
vite/.gitignore
vendored
Normal file
4
vite/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
1485
vite/package-lock.json
generated
Normal file
1485
vite/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
vite/package.json
Normal file
23
vite/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "openai",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "APP_ENV=development vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"sass": "^1.77.8",
|
||||
"svelte": "^4.2.19",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-live-reload": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.14.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"clipboard": "^2.0.11",
|
||||
"highlight.js": "^11.10.0"
|
||||
}
|
||||
}
|
||||
76
vite/src/AssistantForm.svelte
Normal file
76
vite/src/AssistantForm.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let assistant = {
|
||||
title: '',
|
||||
model: 'gpt-3.5',
|
||||
system_prompt: '',
|
||||
temperature: 1.0,
|
||||
top_p: 1.0,
|
||||
};
|
||||
|
||||
export let models = [
|
||||
'gpt-4o', 'gpt-3.5', 'gpt-4'
|
||||
];
|
||||
|
||||
let errors = {}
|
||||
|
||||
export function setErrors(data) {
|
||||
errors = data;
|
||||
}
|
||||
|
||||
export function setAssistant(data) {
|
||||
assistant = data;
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
// Émet l'événement 'save' avec les données de l'assistant
|
||||
dispatch('save', assistant);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<input type="text" class="form-control {errors.title ? 'is-invalid' : ''}" placeholder="Titre" bind:value={assistant.title}>
|
||||
{#if errors.system_prompt}
|
||||
<div class="invalid-feedback">{errors.system_prompt}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label for="assistant-model">Model</label>
|
||||
<select class="form-select" bind:value={assistant.model}>
|
||||
{#each models as model}
|
||||
<option value={model}>{model}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label for="assistant-system_prompt">Prompt système</label>
|
||||
<textarea class="form-control {errors.system_prompt ? 'is-invalid' : ''}" rows="3" bind:value={assistant.system_prompt}></textarea>
|
||||
{#if errors.system_prompt}
|
||||
<div class="invalid-feedback">{errors.system_prompt}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label for="assistant-temperature" class="form-label">Température</label>
|
||||
<input type="range" min="0" max="2" step="0.01" bind:value={assistant.temperature} class="form-range">
|
||||
<output>{assistant.temperature}</output>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label for="assistant-top_p" class="form-label">Top p</label>
|
||||
<input type="range" min="0" max="1" step="0.01" bind:value={assistant.top_p} class="form-range">
|
||||
<output>{assistant.top_p}</output>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button class="btn btn-primary" on:click={handleSave}>Enregistrer</button>
|
||||
</div>
|
||||
|
||||
529
vite/src/ChatApp.svelte
Normal file
529
vite/src/ChatApp.svelte
Normal file
@@ -0,0 +1,529 @@
|
||||
<script>
|
||||
import markdownit from 'markdown-it'
|
||||
import hljs from 'highlight.js';
|
||||
// import ClipboardJS from 'clipboard';
|
||||
import ChatMessage from './ChatMessage.svelte';
|
||||
import Modal from './Modal.svelte';
|
||||
import AssistantForm from './AssistantForm.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// export let apiKey = '';
|
||||
export let proxyBaseUrl = '';
|
||||
export let model_list = [];
|
||||
|
||||
let assistant = null;
|
||||
let assistant_id = 0;
|
||||
let assistant_title = '';
|
||||
let assistants = [];
|
||||
|
||||
let messages = [];
|
||||
let newMessage = '';
|
||||
let chatContainer;
|
||||
let assistantForm; // Reference to the AssistantForm component
|
||||
let modal; // Reference to the Modal component
|
||||
|
||||
onMount(() => {
|
||||
refreshAssistants();
|
||||
});
|
||||
|
||||
const md = markdownit({
|
||||
linkify: true,
|
||||
|
||||
highlight(code, lang) {
|
||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||
const html = hljs.highlight(code, {language: language, ignoreIllegals: true }).value
|
||||
return `<pre class="hljs-code-container my-3"><div class="hljs-code-header"><span>${language}</span><button class="hljs-copy-button">Copy</button></div><code class="hljs language-${language}">${html}</code></pre>`
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
new ClipboardJS('.hljs-copy-button', {
|
||||
target: function(trigger) {
|
||||
console.log(trigger.parentNode.nextElementSibling)
|
||||
return trigger.parentNode.nextElementSibling;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
async function postRequest(url, headers, body) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function readStream(stream, progressCallback) {
|
||||
const reader = stream.getReader();
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
let responseObj = {};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const lines = textDecoder.decode(value).split("\n");
|
||||
processLines(lines, responseObj, progressCallback);
|
||||
}
|
||||
|
||||
return responseObj;
|
||||
}
|
||||
|
||||
function processLines(lines, responseObj, progressCallback) {
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
if (line.includes("[DONE]")) {
|
||||
return responseObj;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
const delta = data.choices[0].delta;
|
||||
|
||||
Object.keys(delta).forEach(key => {
|
||||
responseObj[key] = (responseObj[key] || "") + delta[key];
|
||||
progressCallback(responseObj);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.log("Error parsing line:", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function complete(messages, apiUrl,/* token,*/ params, progressCallback) {
|
||||
// const apiUrl = baseUrl + '/chat/completions';
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
// "Authorization": `Bearer ${token}`
|
||||
};
|
||||
|
||||
const body = {
|
||||
model: params.model,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
if (params.temperature != undefined) {
|
||||
body.temperature = params.temperature;
|
||||
}
|
||||
|
||||
if (params.top_p != undefined) {
|
||||
body.top_p = params.top_p;
|
||||
}
|
||||
|
||||
const response = await postRequest(apiUrl, headers, body);
|
||||
return readStream(response.body, progressCallback);
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (newMessage.trim() === '') return;
|
||||
|
||||
if (!messages.length && assistant) {
|
||||
const systemMessage = { role: 'system', content: assistant.system_prompt };
|
||||
messages.push(systemMessage);
|
||||
}
|
||||
|
||||
const userMessage = { role: 'user', content: newMessage };
|
||||
|
||||
messages.push(userMessage);
|
||||
messages.push({ role: 'assistant', content: '' });
|
||||
messages = messages;
|
||||
const lastMsgIndex = messages.length - 1;
|
||||
|
||||
try {
|
||||
if (assistant) {
|
||||
complete(
|
||||
messages,
|
||||
proxyBaseUrl,
|
||||
// apiKey,
|
||||
assistant,
|
||||
(message) => {
|
||||
if (message.content)
|
||||
messages[lastMsgIndex].content = message.content;
|
||||
}
|
||||
);
|
||||
} else if (!assistants.length) {
|
||||
messages[lastMsgIndex].content = "Aucun assistant n'a été trouvé. Veuillez en créer un nouveau en cliquant sur le bouton +.";
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return;
|
||||
} finally {
|
||||
newMessage = '';
|
||||
}
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
function loadAssistant(config = {}) {
|
||||
assistant = null;
|
||||
|
||||
// ES6 Destructuring object properties into variables with default values
|
||||
const {
|
||||
model = '',
|
||||
system_prompt = '',
|
||||
temperature = 0,
|
||||
top_p = 0
|
||||
} = config;
|
||||
|
||||
// ES6 (Property Shorthand)
|
||||
assistant = {model, system_prompt, temperature, top_p };
|
||||
|
||||
assistant_id = config.id;
|
||||
assistant_title = config.title;
|
||||
|
||||
console.log("Changed assistant " + assistant_id);
|
||||
console.log(assistant);
|
||||
|
||||
}
|
||||
|
||||
async function refreshAssistants() {
|
||||
|
||||
try {
|
||||
const response = await fetch('/site/assistant/list', {method: 'GET'});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch assistants');
|
||||
}
|
||||
|
||||
assistants = await response.json();
|
||||
|
||||
if (assistants.length) {
|
||||
assistants.forEach(config => {
|
||||
if (config.default == 1) {
|
||||
loadAssistant(config);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (!assistant) {
|
||||
loadAssistant(assistants[0]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing assistants:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle saving assistant data
|
||||
async function handleSave(event) {
|
||||
try {
|
||||
const assistantData = event.detail;
|
||||
|
||||
const response = await fetch('/site/assistant/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(assistantData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Server response error');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status == 'success') {
|
||||
refreshAssistants();
|
||||
modal.hide();
|
||||
} else if (data.status == 'error' && data.errors) {
|
||||
assistantForm.setErrors(data.errors);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving assistant:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle changing assistant
|
||||
async function handleChange() {
|
||||
try {
|
||||
|
||||
assistants.forEach(config => {
|
||||
if (config.id == assistant_id) {
|
||||
loadAssistant(config);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch('/site/assistant/set-as-default?id=' + assistant_id);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Server response error');
|
||||
}
|
||||
|
||||
const status = await response.json();
|
||||
|
||||
if (!status) {
|
||||
throw new Error('Failed to set default assistant');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error changing assistant:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createAssistant()
|
||||
{
|
||||
assistantForm.setAssistant({
|
||||
title: '',
|
||||
model: 'gpt-4o',
|
||||
system_prompt: '',
|
||||
temperature: 0,
|
||||
top_p: 1.0,
|
||||
default: 1
|
||||
});
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function editAssistant()
|
||||
{
|
||||
assistants.forEach(config => {
|
||||
if (config.id == assistant_id) {
|
||||
assistantForm.setAssistant(config);
|
||||
modal.show();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteAssistant()
|
||||
{
|
||||
if (assistant && confirm(`Êtes-vous certain de vouloir effacer l'assistant ${assistant_title} ?`)) {
|
||||
try {
|
||||
|
||||
const response = await fetch('/site/assistant/delete?id=' + assistant_id);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Server response error');
|
||||
}
|
||||
|
||||
const status = await response.json();
|
||||
|
||||
if (!status) {
|
||||
throw new Error('Failed to delete assistant');
|
||||
}
|
||||
|
||||
assistant = null;
|
||||
|
||||
refreshAssistants();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting assistant:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exportMessages() {
|
||||
const date = new Date();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${day}-${month}-${date.getFullYear()}`;
|
||||
const formattedTime = `${hours}-${minutes}-${seconds}`;
|
||||
|
||||
const filename = `${assistant_title}_${formattedDate}_${formattedTime}.json`;
|
||||
|
||||
const json = JSON.stringify(messages, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
// Fonction pour importer les messages depuis un fichier JSON
|
||||
function importMessages(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const importedMessages = JSON.parse(e.target.result);
|
||||
if (Array.isArray(importedMessages)) {
|
||||
messages = importedMessages;
|
||||
} else {
|
||||
throw new Error("Invalid JSON format");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error importing messages:", error);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
/*
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let messages = writable([]);
|
||||
let newMessage = writable("");
|
||||
|
||||
const sendMessage = () => {
|
||||
if ($newMessage.trim() !== "") {
|
||||
messages.update(msgs => [...msgs, { role: 'user', content: $newMessage }]);
|
||||
newMessage.set("");
|
||||
}
|
||||
};
|
||||
|
||||
// Sample function to simulate receiving a message from the AI
|
||||
const receiveMessage = (content) => {
|
||||
messages.update(msgs => [...msgs, { role: 'assistant', content }]);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
// Simulate receiving an initial message from the AI
|
||||
receiveMessage("Hello! How can I assist you today?");
|
||||
});
|
||||
*/
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h1 class="text-center">{assistant_title}</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-primary" on:click={createAssistant} title="Créer un assistant"><span class="icon-add"></span></button>
|
||||
|
||||
{#if assistants.length}
|
||||
<select class="form-select" bind:value={assistant_id} on:change={handleChange}>
|
||||
{#each assistants as config}
|
||||
<option value={config.id}>{config.title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if assistant}
|
||||
<button class="btn btn-secondary" on:click={editAssistant} title="Modifier l'assistant {assistant_title}"><span class="icon-edit"></span></button>
|
||||
<button class="btn btn-danger" on:click={deleteAssistant} title="Supprimer l'assistant {assistant_title}"><span class="icon-delete"></span></button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
{#if messages.length}
|
||||
<div class="separator d-none d-sm-block"></div>
|
||||
<button class="btn btn-warning clear" on:click="{clearMessages}" title="Effacer les messages"><span class="icon-clean"></button>
|
||||
<button class="btn btn-info" on:click="{exportMessages}" title="Exporter la conversation"><span class="icon-file_download"></span></button>
|
||||
<div class="separator d-none d-sm-block"></div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
<input id="import-file" type="file" accept=".json" on:change="{importMessages}" style="display:none" />
|
||||
<label for="import-file" class="btn btn-info"><span class="icon-file_upload" title="Importer une conversation"></span></label>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="chat-container" bind:this="{chatContainer}" >
|
||||
{#each messages as message, index (index)}
|
||||
<ChatMessage message="{message}" container="{chatContainer.parentElement}" markdown={md} />
|
||||
{/each}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<textarea class="chat-input" rows="1"
|
||||
bind:value="{newMessage}"
|
||||
on:keyup={e => { if (e.key === 'Enter') sendMessage(); }}
|
||||
placeholder="Saisissez votre message ici..."
|
||||
aria-label="Chat with AI"></textarea>
|
||||
</footer>
|
||||
|
||||
<Modal bind:this="{modal}" ariaLabelledby="editAssistantModal">
|
||||
<AssistantForm models={model_list} bind:this={assistantForm} on:save={handleSave} />
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.separator {
|
||||
background-color: #4a4a4a;
|
||||
width: 1px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
& > select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
& > button {
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
button.clear {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
width: 100%;
|
||||
max-width: 768px;
|
||||
padding: 8px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
51
vite/src/ChatMessage.svelte
Normal file
51
vite/src/ChatMessage.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import { beforeUpdate, afterUpdate } from 'svelte';
|
||||
|
||||
export let message;
|
||||
export let container;
|
||||
export let markdown;
|
||||
|
||||
let autoscroll;
|
||||
let renderedContent = '';
|
||||
|
||||
$: renderedContent = markdown.render(message.content);
|
||||
|
||||
beforeUpdate(() => {
|
||||
autoscroll = container && container.offsetHeight + container.scrollTop >
|
||||
container.scrollHeight - 20;
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
if (autoscroll) container.scrollTo(0, container.scrollHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hljs-code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 3px 6px;
|
||||
background-color: #9b9b9b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
|
||||
code:not(.hljs) {
|
||||
color: #4d4d4d;
|
||||
padding: 0 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.assistant {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if message.role != 'system'}
|
||||
<div class={message.role + ' message'}>
|
||||
{@html renderedContent}
|
||||
</div>
|
||||
{/if}
|
||||
37
vite/src/Modal.svelte
Normal file
37
vite/src/Modal.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let modalElement;
|
||||
let modal;
|
||||
export let ariaLabelledby = '';
|
||||
|
||||
onMount(() => {
|
||||
modal = new bootstrap.Modal(modalElement);
|
||||
});
|
||||
|
||||
export function show()
|
||||
{
|
||||
if (modal) modal.show();
|
||||
}
|
||||
|
||||
export function hide()
|
||||
{
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="modal fade" bind:this={modalElement} tabindex="-1" role="dialog" aria-hidden="true" aria-labelledby={ariaLabelledby}>
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
33
vite/src/main.js
Normal file
33
vite/src/main.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Styles
|
||||
import './styles/site.scss'
|
||||
|
||||
import ChatApp from './ChatApp.svelte'
|
||||
|
||||
// import * as bootstrap from 'bootstrap/dist/js/bootstrap';
|
||||
// window.bootstrap = bootstrap;
|
||||
|
||||
import {Modal, Alert} from 'bootstrap';
|
||||
|
||||
window.ChatApp = ChatApp;
|
||||
|
||||
window.bootstrap = {
|
||||
Modal: Modal,
|
||||
Alert: Alert,
|
||||
};
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
/*
|
||||
const hamburgerBtn = document.querySelector('.hamburger')
|
||||
hamburgerBtn.addEventListener('click', function () {
|
||||
this.classList.toggle('is-open')
|
||||
this.classList.toggle('is-closed')
|
||||
})
|
||||
*/
|
||||
|
||||
const activeLink = document.querySelector('#mainmenu a[href="' + location.pathname + '"]');
|
||||
|
||||
if (activeLink) {
|
||||
activeLink.parentNode.classList.add('active');
|
||||
}
|
||||
});
|
||||
155
vite/src/styles/_chat.scss
Normal file
155
vite/src/styles/_chat.scss
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, "system-ui", "Segoe UI Adjusted", "Segoe UI",
|
||||
"Liberation Sans", sans-serif;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
*/
|
||||
#chat-app {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
|
||||
& > button {
|
||||
position: absolute;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
button.menu {
|
||||
left: 0;
|
||||
}
|
||||
button.clear {
|
||||
right: 0;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#input-box {
|
||||
width: 100%;
|
||||
max-width: 768px;
|
||||
padding: 8px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.hljs-code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 3px 6px;
|
||||
background-color: #9b9b9b;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
aside {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
}
|
||||
.sidebar-container {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
width: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar-modal {
|
||||
flex: 1 0;
|
||||
min-width: 64px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
li > button {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
text-align: left;
|
||||
padding: 0 16px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
li > button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
main {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
*/
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.chat-container .message {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
|
||||
.chat-container code:not(.hljs) {
|
||||
color: #4d4d4d;
|
||||
padding: 0 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chat-container .assistant {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
63
vite/src/styles/_fonts.scss
Normal file
63
vite/src/styles/_fonts.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('/fonts/icomoon.eot?ws5e0y');
|
||||
src: url('/fonts/icomoon.eot?ws5e0y#iefix') format('embedded-opentype'),
|
||||
url('/fonts/icomoon.ttf?ws5e0y') format('truetype'),
|
||||
url('/fonts/icomoon.woff?ws5e0y') format('woff'),
|
||||
url('/fonts/icomoon.svg?ws5e0y#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-error:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-warning:before {
|
||||
content: "\e901";
|
||||
}
|
||||
.icon-loop:before {
|
||||
content: "\e902";
|
||||
}
|
||||
.icon-mic:before {
|
||||
content: "\e903";
|
||||
}
|
||||
.icon-library_add:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-add:before {
|
||||
content: "\e904";
|
||||
}
|
||||
.icon-edit:before {
|
||||
content: "\e905";
|
||||
}
|
||||
.icon-file_download:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.icon-file_upload:before {
|
||||
content: "\e909";
|
||||
}
|
||||
.icon-delete:before {
|
||||
content: "\e906";
|
||||
}
|
||||
.icon-settings:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-clean:before {
|
||||
content: "\e907";
|
||||
}
|
||||
113
vite/src/styles/_hamburger.scss
Normal file
113
vite/src/styles/_hamburger.scss
Normal file
@@ -0,0 +1,113 @@
|
||||
// -------------------------------
|
||||
// Hamburger-Cross
|
||||
// -------------------------------
|
||||
|
||||
// https://codepen.io/djdabe/pen/qXgJNV
|
||||
|
||||
.hamburger {
|
||||
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
.hamb-top, .hamb-middle, .hamb-bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
&.is-closed {
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0,0,0);
|
||||
-webkit-transition: all .35s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
&:before {
|
||||
opacity: 1;
|
||||
display: block;
|
||||
transform: translate3d(-100px, 0, 0);
|
||||
transition: all .35s ease-in-out;
|
||||
}
|
||||
|
||||
.hamb-top {
|
||||
top: 0;
|
||||
transition: all .35s ease-in-out;
|
||||
}
|
||||
.hamb-bottom {
|
||||
bottom: 0;
|
||||
transition: all .35s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.hamb-top {
|
||||
top: 5px;
|
||||
-webkit-transition: all .35s ease-in-out;
|
||||
}
|
||||
.hamb-middle {
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.hamb-bottom {
|
||||
bottom: 5px;
|
||||
-webkit-transition: all .35s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
.hamb-top,
|
||||
.hamb-bottom {
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.hamb-top {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-webkit-transition: -webkit-transform .2s cubic-bezier(.73,1,.28,.08);
|
||||
}
|
||||
.hamb-middle {
|
||||
display: none;
|
||||
}
|
||||
.hamb-bottom {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-webkit-transition: -webkit-transform .2s cubic-bezier(.73,1,.28,.08);
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0,0,0);
|
||||
-webkit-transition: all .35s ease-in-out;
|
||||
}
|
||||
&:hover:before {
|
||||
opacity: 1;
|
||||
display: block;
|
||||
-webkit-transform: translate3d(-100px,0,0);
|
||||
-webkit-transition: all .35s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
66
vite/src/styles/site.scss
Normal file
66
vite/src/styles/site.scss
Normal file
@@ -0,0 +1,66 @@
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "~highlightjs/scss/github";
|
||||
|
||||
@import "fonts";
|
||||
@import "hamburger";
|
||||
|
||||
// @import "chat";
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
|
||||
#navBtn {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#chat-app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#chat {
|
||||
white-space: pre-wrap;
|
||||
|
||||
.user {
|
||||
color: #ffeaa4;
|
||||
}
|
||||
|
||||
.assistant {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-signin {
|
||||
max-width: 330px;
|
||||
padding: 1rem;
|
||||
|
||||
.form-floating:focus-within {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
margin-bottom: 10px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
7
vite/svelte.config.js
Normal file
7
vite/svelte.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
65
vite/vite.config.js
Normal file
65
vite/vite.config.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// View your website at your own local server
|
||||
// for example http://vite-php-setup.test
|
||||
|
||||
// http://localhost:5133 is serving Vite on development
|
||||
// but accessing it directly will be empty
|
||||
// TIP: consider changing the port for each project, see below
|
||||
|
||||
// IMPORTANT image urls in CSS works fine
|
||||
// BUT you need to create a symlink on dev server to map this folder during dev:
|
||||
// ln -s {path_to_project_source}/src/assets {path_to_public_html}/assets
|
||||
// on production everything will work just fine
|
||||
// (this happens because our Vite code is outside the server public access,
|
||||
// if it where, we could use https://vitejs.dev/config/server-options.html#server-origin)
|
||||
|
||||
import { defineConfig, splitVendorChunkPlugin } from 'vite'
|
||||
import liveReload from 'vite-plugin-live-reload'
|
||||
import path from 'path'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import svelteConfig from './svelte.config.js' // Configuration Svelte
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
liveReload([
|
||||
// edit live reload paths according to your source code
|
||||
__dirname + '/../modules/**/*.php',
|
||||
__dirname + '/../config/*.php',
|
||||
__dirname + '/../web/*.php',
|
||||
]),
|
||||
splitVendorChunkPlugin(),
|
||||
svelte(svelteConfig),
|
||||
],
|
||||
root: 'src',
|
||||
base: process.env.APP_ENV === 'development'
|
||||
? '/dev/'
|
||||
: '/',
|
||||
|
||||
build: {
|
||||
// Output dir for production build
|
||||
outDir: '../../web',
|
||||
emptyOutDir: false,
|
||||
|
||||
// Emit manifest so PHP can find the hashed files
|
||||
manifest: true,
|
||||
|
||||
// Our entry
|
||||
rollupOptions: {
|
||||
input: path.resolve(__dirname, 'src/main.js'),
|
||||
}
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
|
||||
'~highlightjs': path.resolve(__dirname, 'node_modules/highlight.js'),
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
// we need a strict port to match on PHP side
|
||||
// change freely, but update on PHP to match the same port
|
||||
strictPort: true,
|
||||
port: 5133
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user