Skip to content

Commit 8c8bc78

Browse files
committed
feat: audio example
1 parent d1d3724 commit 8c8bc78

18 files changed

+382
-26
lines changed

assets/app.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import './bootstrap.js';
22
import 'bootstrap/dist/css/bootstrap.min.css';
33
import './styles/app.css';
4+
import './styles/audio.css';
45
import './styles/blog.css';
56
import './styles/youtube.css';
67
import './styles/wikipedia.css';
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import { getComponent } from '@symfony/ux-live-component';
3+
4+
export default class extends Controller {
5+
async initialize() {
6+
this.component = await getComponent(this.element);
7+
this.scrollToBottom();
8+
9+
const resetButton = document.getElementById('chat-reset');
10+
resetButton.addEventListener('click', (event) => {
11+
this.component.action('reset');
12+
});
13+
14+
const startButton = document.getElementById('micro-start');
15+
const stopButton = document.getElementById('micro-stop');
16+
const botThinkingButton = document.getElementById('bot-thinking');
17+
18+
startButton.addEventListener('click', (event) => {
19+
event.preventDefault();
20+
startButton.classList.add('d-none');
21+
stopButton.classList.remove('d-none');
22+
this.startRecording();
23+
});
24+
stopButton.addEventListener('click', (event) => {
25+
event.preventDefault();
26+
stopButton.classList.add('d-none');
27+
botThinkingButton.classList.remove('d-none');
28+
this.mediaRecorder.stop();
29+
});
30+
31+
this.component.on('loading.state:started', (e,r) => {
32+
if (r.actions.includes('reset')) {
33+
return;
34+
}
35+
document.getElementById('welcome')?.remove();
36+
document.getElementById('loading-message').removeAttribute('class');
37+
this.scrollToBottom();
38+
});
39+
40+
this.component.on('loading.state:finished', () => {
41+
document.getElementById('loading-message').setAttribute('class', 'd-none');
42+
botThinkingButton.classList.add('d-none');
43+
startButton.classList.remove('d-none');
44+
});
45+
46+
this.component.on('render:finished', () => {
47+
this.scrollToBottom();
48+
});
49+
};
50+
51+
async startRecording() {
52+
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
53+
this.mediaRecorder = new MediaRecorder(stream);
54+
let audioChunks = [];
55+
56+
this.mediaRecorder.ondataavailable = (event) => {
57+
audioChunks.push(event.data);
58+
};
59+
60+
this.mediaRecorder.onstop = async () => {
61+
const audioBlob = new Blob(audioChunks, {type: 'audio/wav'});
62+
63+
const base64String = await this.blobToBase64(audioBlob);
64+
this.component.action('submit', { audio: base64String });
65+
};
66+
67+
this.mediaRecorder.start();
68+
}
69+
70+
scrollToBottom() {
71+
const chatBody = document.getElementById('chat-body');
72+
chatBody.scrollTop = chatBody.scrollHeight;
73+
}
74+
75+
blobToBase64(blob) {
76+
return new Promise((resolve) => {
77+
const reader = new FileReader();
78+
reader.readAsDataURL(blob);
79+
reader.onloadend = () => resolve(reader.result.split(',')[1]);
80+
});
81+
}
82+
83+
playBase64Audio(base64String) {
84+
const audioSrc = "data:audio/wav;base64," + base64String;
85+
const audio = new Audio(audioSrc);
86+
87+
audio.play().catch(error => console.error("Playback error:", error));
88+
}
89+
}
Loading
Loading
Loading

assets/icons/iconoir/timer-solid.svg

+1
Loading

assets/styles/audio.css

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.audio {
2+
body&, .card-img-top {
3+
background: #df662f;
4+
background: linear-gradient(0deg, #df662f 0%, #a80a1d 100%);
5+
}
6+
7+
.card-img-top {
8+
color: #ffffff;
9+
}
10+
11+
&.chat {
12+
.user-message {
13+
background: #df662f;
14+
color: #ffffff;
15+
}
16+
17+
.bot-message {
18+
color: #ffffff;
19+
background: #215d9a;
20+
21+
&.loading {
22+
color: rgba(255, 255, 255, 0.5);
23+
}
24+
25+
a {
26+
color: #c8d8ef;
27+
28+
&:hover {
29+
color: #ffffff;
30+
}
31+
}
32+
33+
code {
34+
color: #ffb1ca;
35+
}
36+
}
37+
38+
.avatar {
39+
&.bot {
40+
outline: 1px solid #c0dbf4;
41+
background: #c0dbf4;
42+
}
43+
44+
&.user {
45+
outline: 1px solid #f3b396;
46+
background: #f3b396;
47+
}
48+
}
49+
50+
#welcome h4 {
51+
color: #2c5282;
52+
}
53+
54+
#chat-reset, #chat-submit {
55+
&:hover {
56+
background: #a80a1d;
57+
border-color: #a80a1d;
58+
}
59+
}
60+
}
61+
}

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"ext-iconv": "*",
1010
"codewithkyrian/chromadb-php": "^0.3.0",
1111
"league/commonmark": "^2.6",
12+
"php-llm/llm-chain": "dev-feat-whisper-support as 0.17.0",
1213
"php-llm/llm-chain-bundle": "^0.17",
1314
"runtime/frankenphp-symfony": "^0.2.0",
1415
"symfony/asset": "7.2.*",

composer.lock

+29-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/packages/llm_chain.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ llm_chain:
3737
system_prompt: 'Please answer the users question based on Wikipedia and provide a link to the article.'
3838
tools:
3939
- 'PhpLlm\LlmChain\Chain\ToolBox\Tool\Wikipedia'
40+
audio:
41+
model:
42+
name: 'GPT'
43+
version: 'gpt-4o-mini'
44+
system_prompt: 'You are a friendly chatbot that likes to have a conversation with users and asks them some questions.'
45+
tools: false
4046
store:
4147
chroma_db:
4248
symfonycon:

config/routes.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ blog:
1111
template: 'chat.html.twig'
1212
context: { chat: 'blog' }
1313

14+
audio:
15+
path: '/audio'
16+
controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController'
17+
defaults:
18+
template: 'chat.html.twig'
19+
context: { chat: 'audio' }
20+
1421
youtube:
1522
path: '/youtube'
1623
controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController'

demo.png

60.3 KB
Loading

0 commit comments

Comments
 (0)