Premier commit

This commit is contained in:
2024-09-09 10:22:45 +02:00
commit bcc2604080
74 changed files with 25819 additions and 0 deletions

4
vite/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
.DS_Store
dist
*.local

1485
vite/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
vite/package.json Normal file
View 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"
}
}

View 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
View 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>

View 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
View 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
View 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
View 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;
}

View 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";
}

View 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
View 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
View 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
View 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
},
})