jonathanjordan21 commited on
Commit
024f45f
·
1 Parent(s): 9d134ac

update UI v0.1

Browse files
.gitignore CHANGED
@@ -22,3 +22,5 @@ dist-ssr
22
  *.njsproj
23
  *.sln
24
  *.sw?
 
 
 
22
  *.njsproj
23
  *.sln
24
  *.sw?
25
+
26
+ .env
chat_history.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Interview Chat History</title>
6
+ <link rel="stylesheet" href="./chat_style.css" />
7
+ </head>
8
+ <body>
9
+
10
+ <h2>Riwayat Chat</h2>
11
+
12
+ <div class="chat-container" id="chatContainer"></div>
13
+
14
+ <script src="./chat_history.js"></script>
15
+ </body>
16
+ </html> -->
17
+
18
+ <!DOCTYPE html>
19
+ <html>
20
+ <head>
21
+ <meta charset="UTF-8" />
22
+ <title>Interview History</title>
23
+ <link rel="stylesheet" href="./chat_style.css" />
24
+ </head>
25
+ <body>
26
+
27
+ <div class="app">
28
+
29
+ <!-- Header -->
30
+ <div class="header">
31
+ <div class="header-left">
32
+ <button id="backBtn" class="back-btn">←</button>
33
+ <div>
34
+ <div class="title">AI Interview Summary</div>
35
+ <div class="subtitle" id="interviewDate"></div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Chat area -->
41
+ <div class="chat-container" id="chatContainer"></div>
42
+
43
+ <button id="rawBtn" class="raw-btn">Raw JSON</button>
44
+
45
+ </div>
46
+
47
+ <script src="./chat_history.js"></script>
48
+ </body>
49
+ </html>
finished.html ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Interview Complete</title>
7
+
8
+ <!-- Google Font -->
9
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
10
+
11
+ <link rel="stylesheet" href="./interview_end.css" />
12
+
13
+ <style>
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ font-family: 'Poppins', sans-serif;
19
+ }
20
+
21
+ body {
22
+ height: 100vh;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ background: linear-gradient(135deg, #3B82F6, #6366F1);
27
+ }
28
+
29
+ .app {
30
+ background: #ffffff;
31
+ padding: 55px 45px;
32
+ border-radius: 20px;
33
+ text-align: center;
34
+ max-width: 520px;
35
+ width: 90%;
36
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
37
+ animation: fadeIn 0.6s ease-in-out;
38
+ position: relative;
39
+ }
40
+
41
+ /* Subtle top accent bar */
42
+ .app::before {
43
+ content: "";
44
+ position: absolute;
45
+ top: 0;
46
+ left: 0;
47
+ right: 0;
48
+ height: 6px;
49
+ border-radius: 20px 20px 0 0;
50
+ background: linear-gradient(90deg, #F97316, #3B82F6);
51
+ }
52
+
53
+ h1 {
54
+ font-size: 30px;
55
+ font-weight: 600;
56
+ color: #1f2937;
57
+ margin-bottom: 22px;
58
+ }
59
+
60
+ p {
61
+ font-size: 16px;
62
+ color: #4b5563;
63
+ margin-bottom: 15px;
64
+ line-height: 1.7;
65
+ }
66
+
67
+ .note {
68
+ font-size: 14px;
69
+ color: #6b7280;
70
+ margin-top: 20px;
71
+ }
72
+
73
+ .highlight {
74
+ color: #F97316;
75
+ font-weight: 600;
76
+ }
77
+
78
+ .btn-home {
79
+ display: inline-block;
80
+ margin-top: 28px;
81
+ padding: 13px 28px;
82
+ border-radius: 10px;
83
+ text-decoration: none;
84
+ font-weight: 600;
85
+ background: linear-gradient(135deg, #F97316, #FB923C);
86
+ color: #fff;
87
+ transition: all 0.3s ease;
88
+ }
89
+
90
+ .btn-home:hover {
91
+ transform: translateY(-3px);
92
+ box-shadow: 0 10px 25px rgba(249, 115, 22, 0.4);
93
+ }
94
+
95
+ @keyframes fadeIn {
96
+ from {
97
+ opacity: 0;
98
+ transform: translateY(25px);
99
+ }
100
+ to {
101
+ opacity: 1;
102
+ transform: translateY(0);
103
+ }
104
+ }
105
+
106
+ @media (max-width: 480px) {
107
+ .app {
108
+ padding: 35px 25px;
109
+ }
110
+
111
+ h1 {
112
+ font-size: 24px;
113
+ }
114
+
115
+ p {
116
+ font-size: 14px;
117
+ }
118
+ }
119
+ </style>
120
+ </head>
121
+ <body>
122
+
123
+ <div class="app">
124
+ <h1>🎉 Interview Complete</h1>
125
+ <p>Thank you for participating in our interview process.</p>
126
+ <p>
127
+ Please wait for further confirmation. You will hear from us within
128
+ <span class="highlight">2 weeks</span>.
129
+ </p>
130
+ <p class="note">We truly appreciate your time and effort. Good luck!</p>
131
+
132
+ <!-- Optional Button -->
133
+ <!-- <a href="/" class="btn-home">Return to Home</a> -->
134
+ </div>
135
+
136
+ </body>
137
+ </html>
index.html CHANGED
@@ -24,7 +24,19 @@
24
  <div id="app">
25
  <!-- Header -->
26
  <header class="top-bar">
27
- <span class="meeting-title">AI Interview Session</span>
 
 
 
 
 
 
 
 
 
 
 
 
28
  <span class="meeting-time">00:00</span>
29
  </header>
30
 
@@ -49,12 +61,12 @@
49
  <main class="meeting-area">
50
  <div class="video-tile ai">
51
  <canvas id="ai-bg"></canvas>
52
- <span class="label">🤖 AI Interviewer</span>
53
  </div>
54
 
55
  <div class="video-tile user">
56
  <video id="userVideo" autoplay playsinline muted></video>
57
- <span class="label">👤 You</span>
58
  </div>
59
  </main>
60
 
@@ -62,7 +74,7 @@
62
  <input
63
  id="apiKeyInput"
64
  type="password"
65
- placeholder="Enter API Key"
66
  autocomplete="off"
67
  class="api-key-input"
68
  />
 
24
  <div id="app">
25
  <!-- Header -->
26
  <header class="top-bar">
27
+ <!-- <span class="meeting-title">AI Interview Session</span> -->
28
+ <div class="status-container">
29
+ <img src="https://static.wixstatic.com/media/b1fac1_9c0ef77531834b2f9be1ea02ff6c47a9~mv2.png"
30
+ alt="LMD Logo"
31
+ class="status-logo"
32
+ >
33
+
34
+ <h1 class="meeting-title">JOSS AI Interview</h1>
35
+
36
+ <!-- <p class="meeting-title">
37
+ JOSS AI Interview
38
+ </p> -->
39
+ </div>
40
  <span class="meeting-time">00:00</span>
41
  </header>
42
 
 
61
  <main class="meeting-area">
62
  <div class="video-tile ai">
63
  <canvas id="ai-bg"></canvas>
64
+ <span class="label">AI Interviewer</span>
65
  </div>
66
 
67
  <div class="video-tile user">
68
  <video id="userVideo" autoplay playsinline muted></video>
69
+ <span class="label">You</span>
70
  </div>
71
  </main>
72
 
 
74
  <input
75
  id="apiKeyInput"
76
  type="password"
77
+ placeholder="Enter Token"
78
  autocomplete="off"
79
  class="api-key-input"
80
  />
package-lock.json CHANGED
@@ -9,6 +9,7 @@
9
  "version": "0.0.0",
10
  "dependencies": {
11
  "@openai/agents": "^0.4.4",
 
12
  "zod": "^4.3.6"
13
  },
14
  "devDependencies": {
@@ -1171,6 +1172,18 @@
1171
  "node": ">= 0.8"
1172
  }
1173
  },
 
 
 
 
 
 
 
 
 
 
 
 
1174
  "node_modules/dunder-proto": {
1175
  "version": "1.0.1",
1176
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
 
9
  "version": "0.0.0",
10
  "dependencies": {
11
  "@openai/agents": "^0.4.4",
12
+ "dotenv": "^17.3.1",
13
  "zod": "^4.3.6"
14
  },
15
  "devDependencies": {
 
1172
  "node": ">= 0.8"
1173
  }
1174
  },
1175
+ "node_modules/dotenv": {
1176
+ "version": "17.3.1",
1177
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
1178
+ "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
1179
+ "license": "BSD-2-Clause",
1180
+ "engines": {
1181
+ "node": ">=12"
1182
+ },
1183
+ "funding": {
1184
+ "url": "https://dotenvx.com"
1185
+ }
1186
+ },
1187
  "node_modules/dunder-proto": {
1188
  "version": "1.0.1",
1189
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
package.json CHANGED
@@ -14,6 +14,7 @@
14
  },
15
  "dependencies": {
16
  "@openai/agents": "^0.4.4",
 
17
  "zod": "^4.3.6"
18
  }
19
  }
 
14
  },
15
  "dependencies": {
16
  "@openai/agents": "^0.4.4",
17
+ "dotenv": "^17.3.1",
18
  "zod": "^4.3.6"
19
  }
20
  }
public/chat_history.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // const chatContainer = document.getElementById("chatContainer")
2
+
3
+ // const data = sessionStorage.getItem("interviewTranscript")
4
+
5
+ // if (!data) {
6
+ // chatContainer.innerHTML = "<p style='color:white'>No transcript found.</p>"
7
+ // } else {
8
+ // const transcript = JSON.parse(data)
9
+
10
+ // console.log(transcript)
11
+
12
+ // transcript.forEach(msg => {
13
+ // const div = document.createElement("div")
14
+ // div.classList.add("message", msg.role)
15
+ // div.textContent = msg.content[0].transcript
16
+ // chatContainer.appendChild(div)
17
+ // })
18
+ // }
19
+
20
+
21
+ const chatContainer = document.getElementById("chatContainer")
22
+ const interviewDate = document.getElementById("interviewDate")
23
+ const backBtn = document.getElementById("backBtn")
24
+ const rawBtn = document.getElementById("rawBtn")
25
+
26
+
27
+ // Back button
28
+ backBtn.addEventListener("click", () => {
29
+ window.location.href = "index.html"
30
+ })
31
+
32
+ rawBtn.addEventListener("click", () => {
33
+ window.location.href = "raw_history.html"
34
+ })
35
+
36
+
37
+ // Set date
38
+ interviewDate.textContent = new Date().toLocaleString()
39
+
40
+ const data = sessionStorage.getItem("interviewTranscript")
41
+
42
+ if (!data) {
43
+ chatContainer.innerHTML =
44
+ "<p style='color:#8696a0'>No transcript found.</p>"
45
+ } else {
46
+ const transcript = JSON.parse(data)
47
+
48
+ transcript.forEach(msg => {
49
+ const div = document.createElement("div")
50
+ div.classList.add("message", msg.role)
51
+ // div.textContent = msg.content[0].transcript
52
+
53
+ div.innerHTML = `
54
+ ${msg.content[0].transcript}
55
+ <div class="timestamp">
56
+ ${new Date().toLocaleTimeString()}
57
+ </div>
58
+ `
59
+
60
+ chatContainer.appendChild(div)
61
+ })
62
+
63
+ chatContainer.scrollTop = chatContainer.scrollHeight
64
+ }
public/chat_style.css ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ font-family: system-ui, -apple-system, BlinkMacSystemFont;
4
+ background: #0b141a;
5
+ color: white;
6
+ height: 100vh;
7
+ display: flex;
8
+ }
9
+
10
+ .app {
11
+ display: flex;
12
+ flex-direction: column;
13
+ width: 100%;
14
+ }
15
+
16
+ /* HEADER */
17
+ .header {
18
+ background: #202c33;
19
+ padding: 12px 16px;
20
+ display: flex;
21
+ align-items: center;
22
+ border-bottom: 1px solid #2a3942;
23
+ }
24
+
25
+ .header-left {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 12px;
29
+ }
30
+
31
+ .back-btn {
32
+ background: none;
33
+ border: none;
34
+ color: #00a884;
35
+ font-size: 18px;
36
+ cursor: pointer;
37
+ }
38
+
39
+ .title {
40
+ font-size: 16px;
41
+ font-weight: 600;
42
+ }
43
+
44
+ .subtitle {
45
+ font-size: 12px;
46
+ color: #8696a0;
47
+ }
48
+
49
+ /* CHAT AREA */
50
+ .chat-container {
51
+ flex: 1;
52
+ padding: 20px;
53
+ overflow-y: auto;
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 10px;
57
+ }
58
+
59
+ /* MESSAGE BUBBLES */
60
+ .message {
61
+ max-width: 65%;
62
+ padding: 10px 14px;
63
+ border-radius: 16px;
64
+ font-size: 14px;
65
+ line-height: 1.4;
66
+ animation: fadeIn 0.2s ease-in-out;
67
+ }
68
+
69
+ .user {
70
+ align-self: flex-end;
71
+ background: #005c4b;
72
+ color: white;
73
+ border-bottom-right-radius: 4px;
74
+ }
75
+
76
+ .assistant {
77
+ align-self: flex-start;
78
+ background: #202c33;
79
+ border-bottom-left-radius: 4px;
80
+ }
81
+
82
+ .timestamp {
83
+ font-size: 10px;
84
+ margin-top: 4px;
85
+ opacity: 0.6;
86
+ text-align: right;
87
+ }
88
+
89
+ .raw-btn {
90
+ margin-left: auto;
91
+ background: #00a884;
92
+ border: none;
93
+ padding: 6px 12px;
94
+ border-radius: 6px;
95
+ cursor: pointer;
96
+ font-size: 12px;
97
+ font-weight: 500;
98
+ color: white;
99
+ }
100
+
101
+
102
+ @keyframes fadeIn {
103
+ from {
104
+ opacity: 0;
105
+ transform: translateY(5px);
106
+ }
107
+ to {
108
+ opacity: 1;
109
+ transform: translateY(0);
110
+ }
111
+ }
public/interview_end.css ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ font-family: 'Poppins', sans-serif;
6
+ }
7
+
8
+ body {
9
+ height: 100vh;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ background: linear-gradient(135deg, #667eea, #764ba2);
14
+ }
15
+
16
+ .app {
17
+ background: #ffffff;
18
+ padding: 50px 40px;
19
+ border-radius: 16px;
20
+ text-align: center;
21
+ max-width: 500px;
22
+ width: 90%;
23
+ box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
24
+ animation: fadeIn 0.6s ease-in-out;
25
+ }
26
+
27
+ h1 {
28
+ font-size: 28px;
29
+ font-weight: 600;
30
+ color: #333;
31
+ margin-bottom: 20px;
32
+ }
33
+
34
+ p {
35
+ font-size: 16px;
36
+ color: #555;
37
+ margin-bottom: 15px;
38
+ line-height: 1.6;
39
+ }
40
+
41
+ .note {
42
+ font-size: 14px;
43
+ color: #888;
44
+ margin-top: 20px;
45
+ }
46
+
47
+ .highlight {
48
+ color: #764ba2;
49
+ font-weight: 600;
50
+ }
51
+
52
+ .btn-home {
53
+ display: inline-block;
54
+ margin-top: 25px;
55
+ padding: 12px 25px;
56
+ border-radius: 8px;
57
+ text-decoration: none;
58
+ font-weight: 500;
59
+ background: #764ba2;
60
+ color: #fff;
61
+ transition: all 0.3s ease;
62
+ }
63
+
64
+ .btn-home:hover {
65
+ background: #5a3791;
66
+ transform: translateY(-2px);
67
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
68
+ }
69
+
70
+ @keyframes fadeIn {
71
+ from {
72
+ opacity: 0;
73
+ transform: translateY(20px);
74
+ }
75
+ to {
76
+ opacity: 1;
77
+ transform: translateY(0);
78
+ }
79
+ }
80
+
81
+ @media (max-width: 480px) {
82
+ .app {
83
+ padding: 35px 25px;
84
+ }
85
+
86
+ h1 {
87
+ font-size: 22px;
88
+ }
89
+
90
+ p {
91
+ font-size: 14px;
92
+ }
93
+ }
public/interview_end.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Optional: Automatically redirect to home after X seconds
2
+ const redirectTime = 15; // seconds
3
+ let countdown = redirectTime;
4
+ const btn = document.querySelector('.btn-home');
5
+
6
+ const timer = setInterval(() => {
7
+ countdown--;
8
+ btn.textContent = `Return to Home (${countdown}s)`;
9
+ if (countdown <= 0) {
10
+ clearInterval(timer);
11
+ window.location.href = '/';
12
+ }}, 1000);
src/counter.js CHANGED
@@ -1,57 +1,129 @@
1
  // import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
2
  import { RealtimeAgent, RealtimeSession } from "https://esm.sh/@openai/agents/realtime"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  export async function setupCounter(button) {
5
  let started = false
6
 
7
  button.addEventListener('click', async () => {
8
- if (started) return
9
-
10
  const apiKeyInput = document.querySelector('#apiKeyInput')
11
 
12
-
13
- if (!apiKeyInput) {
14
- throw new Error('apiKeyInput (#apiKeyInput) not found')
15
- }
16
 
17
-
18
- // const apiKeyInput = document.querySelector<HTMLInputElement>('#apiKeyInput')!
 
 
 
 
 
 
 
19
 
20
  const apiKey = apiKeyInput.value.trim()
21
 
22
  if (!apiKey) {
23
- alert('Please enter your API key')
24
  apiKeyInput.focus()
25
  return
26
  }
27
 
 
 
 
28
  try {
29
- const res = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
 
 
 
30
  method: "POST",
31
- headers: {
32
- "Authorization": "Bearer " + apiKey,
33
- "Content-Type": "application/json"
34
- },
35
- body: JSON.stringify({"session": {
36
- "type": "realtime",
37
- "model": "gpt-realtime"
38
- }}),
39
- })
40
-
41
- if (!res.ok) {
42
- throw new Error(`Request failed: ${res.status}`)
43
  }
44
-
45
- const data = await res.json()
46
- console.log(data)
47
 
 
 
 
 
 
 
 
 
 
 
48
  started = true
49
 
50
- button.textContent = 'Interview Live'
 
 
 
 
 
 
51
 
52
  await startUserCamera()
53
  startAIBackground()
54
- startAIVoiceInterview(data.value)
 
55
 
56
  } catch (err) {
57
  alert(
@@ -59,25 +131,164 @@ export async function setupCounter(button) {
59
  ? err.message
60
  : "Request failed. Please try again."
61
  )
 
62
  return
63
  }
64
  })
65
  }
66
 
67
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- async function startAIBackground() {
70
 
71
- const canvas = document.querySelector('#ai-bg')
 
 
 
 
 
 
 
 
 
 
 
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  if (!canvas) {
75
  throw new Error('canvas (#ai-bg) not found')
76
  }
77
-
78
  // const canvas = document.querySelector<HTMLCanvasElement>('#ai-bg')!
79
-
80
-
81
  const ctx = canvas.getContext('2d')
82
 
83
  canvas.width = canvas.offsetWidth
@@ -120,11 +331,13 @@ async function startUserCamera() {
120
  }
121
 
122
 
123
- async function startAIVoiceInterview(key) {
124
  const agent = new RealtimeAgent({
125
  name: 'Interviewer',
126
- instructions: 'You are AI Agent currently interviewing candidates in Bahasa Indonesia. Ask everything related to their skill',
 
127
  });
 
128
  const session = new RealtimeSession(agent, {"model":"gpt-realtime"});
129
  try {
130
  await session.connect({
@@ -132,7 +345,19 @@ async function startAIVoiceInterview(key) {
132
  // curl -s -X POST https://api.openai.com/v1/realtime/client_secrets -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{"session": {"type": "realtime", "model": "gpt-realtime"}}' | jq .value
133
  apiKey: key
134
  });
135
- console.log('You are connected!');
 
 
 
 
 
 
 
 
 
 
 
 
136
  } catch (e) {
137
  console.error(e);
138
  }
 
1
  // import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
2
  import { RealtimeAgent, RealtimeSession } from "https://esm.sh/@openai/agents/realtime"
3
+ // import dotenv from "dotenv";
4
+
5
+ // dotenv.config();
6
+
7
+ let mediaRecorder = null
8
+ let recordedChunks = []
9
+ let stream = null
10
+ let chat_history = []
11
+
12
+ let interviewToken = null;
13
+
14
+ const API_BASE_URL = 'https://jonathanjordan21-joss-interview-backend-demo.hf.space'
15
+
16
+ const parts = window.location.pathname.split('/').filter(p => p.trim() !== '');
17
+ const interviewId = parts[parts.length - 1];
18
+
19
+ function formatTime(totalSeconds) {
20
+ const mins = Math.floor(totalSeconds / 60);
21
+ const secs = totalSeconds % 60;
22
+
23
+ return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
24
+ }
25
+
26
+ const timerElement = document.querySelector(".meeting-time");
27
+ let seconds = 0;
28
+
29
+
30
+ if (parts.length != 2) {
31
+ console.error('400 Interview not found');
32
+ const appDiv = document.getElementById('app');
33
+ if (appDiv) appDiv.textContent = '400 Interview not found';
34
+ } else {
35
+ async function loadData() {
36
+ try {
37
+ const response = await fetch(`${API_BASE_URL}/interview/${interviewId}`, {
38
+ // headers: {"Authorization": "Bearer " + HF_API_KEY}
39
+ });
40
+
41
+ if (!response.ok) throw new Error(`${response.status}`);
42
+
43
+ const data = await response.json();
44
+
45
+ } catch (err) {
46
+ console.error(err);
47
+ console.error(err.message)
48
+ const appDiv = document.getElementById('app');
49
+ if (appDiv) appDiv.textContent = `Error fetching interview: ${err.message}`;
50
+ }
51
+ }
52
+
53
+ loadData();
54
+ }
55
 
56
  export async function setupCounter(button) {
57
  let started = false
58
 
59
  button.addEventListener('click', async () => {
 
 
60
  const apiKeyInput = document.querySelector('#apiKeyInput')
61
 
62
+ if (started) {
63
+ button.textContent = 'Loading...'
 
 
64
 
65
+ await stopInterview()
66
+
67
+ // button.textContent = "Start Interview"
68
+ // apiKeyInput.style.display = 'block';
69
+ setTimeout(() => {
70
+ window.location.href = "/finished.html"
71
+ }, 1000)
72
+ return
73
+ }
74
 
75
  const apiKey = apiKeyInput.value.trim()
76
 
77
  if (!apiKey) {
78
+ alert('Please input your token')
79
  apiKeyInput.focus()
80
  return
81
  }
82
 
83
+ button.textContent = 'Loading...'
84
+ interviewToken = apiKey;
85
+
86
  try {
87
+
88
+ console.log("start interviewing....")
89
+
90
+ const resp = await fetch(`${API_BASE_URL}/interview/${interviewId}/start_interview`,{
91
  method: "POST",
92
+ headers: {"Authorization": "Bearer " + interviewToken},
93
+ });
94
+
95
+ if (!resp.ok){
96
+ button.textContent = 'Start Interview'
97
+ const detail = (await resp.json())['detail']
98
+ alert(detail)
99
+ console.log(detail)
100
+ return
 
 
 
101
  }
 
 
 
102
 
103
+ if (resp.status == 202) {
104
+ console.log("Redirecting now...");
105
+ setTimeout(() => {
106
+ window.location.href = "/finished.html"
107
+ }, 1500)
108
+ return;
109
+ }
110
+
111
+ const interviewData = await resp.json()
112
+
113
  started = true
114
 
115
+ button.textContent = 'Stop Interview';
116
+ apiKeyInput.style.display = 'none';
117
+
118
+ setInterval(() => {
119
+ seconds++;
120
+ timerElement.textContent = formatTime(seconds);
121
+ }, 1000);
122
 
123
  await startUserCamera()
124
  startAIBackground()
125
+ startAIVoiceInterview(interviewData.data.ephemeral_key, interviewData.data.prompt)
126
+ await startInterview()
127
 
128
  } catch (err) {
129
  alert(
 
131
  ? err.message
132
  : "Request failed. Please try again."
133
  )
134
+ console.log(err.message)
135
  return
136
  }
137
  })
138
  }
139
 
140
 
141
+ async function startInterview() {
142
+ const video = document.getElementById('userVideo')
143
+ stream = await navigator.mediaDevices.getUserMedia({
144
+ video: true,
145
+ audio: true
146
+ })
147
+ video.srcObject = stream
148
+ await startRecording(stream)
149
+
150
+ }
151
 
 
152
 
153
+ async function startRecording(stream) {
154
+ recordedChunks = []
155
+ mediaRecorder = new MediaRecorder(stream, {
156
+ mimeType: 'video/webm'
157
+ })
158
+ mediaRecorder.ondataavailable = (event) => {
159
+ if (event.data.size > 0) {
160
+ recordedChunks.push(event.data)
161
+ }
162
+ }
163
+ // mediaRecorder.onstop = saveRecording
164
+ mediaRecorder.start()
165
+ }
166
 
167
+
168
+ async function stopInterview(session) {
169
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
170
+ mediaRecorder.stop()
171
+
172
+ console.log(`STOPPING INTERVIEW...`)
173
+
174
+ const blob = new Blob(recordedChunks, { type: "audio/webm" });
175
+ recordedChunks = [];
176
+
177
+ const formData = new FormData();
178
+ formData.append("file", blob, "recording.webm");
179
+
180
+ const upload_recording = await fetch(`${API_BASE_URL}/interview/${interviewId}/file`, {
181
+ method: "POST",
182
+ headers: {"Authorization": "Bearer " + interviewToken},
183
+ body: formData
184
+ })
185
+
186
+ const recording_data = await upload_recording.json()
187
+
188
+ const transcriptData = {
189
+ transcriptId: recording_data["data"]["datetime"],
190
+ date: new Date().toISOString(),
191
+ messages: chat_history
192
+ }
193
+
194
+ const res = await fetch(`${API_BASE_URL}/interview/${interviewId}`, {
195
+ method: "PATCH",
196
+ headers: {
197
+ "Authorization": "Bearer " + interviewToken,
198
+ "Content-Type": "application/json"
199
+ },
200
+ body: JSON.stringify({
201
+ transcript:chat_history,
202
+ recording_url:recording_data["data"]["url"],
203
+ status:"FINISHED",
204
+ duration:seconds
205
+ }),
206
+ })
207
+
208
+ if (!res.ok) {
209
+ throw new Error(`Request failed: ${res.status}`)
210
+ }
211
+
212
+ // const data = await res.json()
213
 
214
+ }
215
+
216
+ if (stream) {
217
+ stream.getTracks().forEach(track => track.stop())
218
+ }
219
+ }
220
+
221
+
222
+ // function saveRecording() {
223
+ // const timestamp = Date.now()
224
+ // const blob = new Blob(recordedChunks, {
225
+ // type: 'video/webm'
226
+ // })
227
+ // const url = URL.createObjectURL(blob)
228
+ // const a = document.createElement('a')
229
+ // a.href = url
230
+ // a.download = `interview_${timestamp}.webm`
231
+ // a.click()
232
+
233
+ // saveInterviewData()
234
+
235
+ // URL.revokeObjectURL(url)
236
+
237
+ // sessionStorage.setItem(
238
+ // "interviewTranscript",
239
+ // JSON.stringify(chat_history)
240
+ // )
241
+
242
+ // }
243
+
244
+
245
+ // function saveInterviewData(timestamp) {
246
+ // const videoBlob = new Blob(recordedChunks, {
247
+ // type: 'video/webm'
248
+ // })
249
+
250
+ // const videoUrl = URL.createObjectURL(videoBlob)
251
+
252
+ // const videoLink = document.createElement('a')
253
+ // videoLink.href = videoUrl
254
+ // videoLink.download = `interview_recording_${timestamp}.webm`
255
+ // videoLink.click()
256
+
257
+ // URL.revokeObjectURL(videoUrl)
258
+
259
+ // const transcriptData = {
260
+ // interviewId: timestamp,
261
+ // date: new Date().toISOString(),
262
+ // messages: chat_history
263
+ // }
264
+
265
+ // const jsonBlob = new Blob(
266
+ // [JSON.stringify(transcriptData, null, 2)],
267
+ // { type: "application/json" }
268
+ // )
269
+
270
+ // const jsonUrl = URL.createObjectURL(jsonBlob)
271
+
272
+ // const jsonLink = document.createElement('a')
273
+ // jsonLink.href = jsonUrl
274
+ // jsonLink.download = `interview_transcript_${timestamp}.json`
275
+ // jsonLink.click()
276
+
277
+ // URL.revokeObjectURL(jsonUrl)
278
+
279
+ // setTimeout(() => {
280
+ // window.location.href = "chat_history.html"
281
+ // }, 1500)
282
+ // }
283
+
284
+
285
+
286
+ async function startAIBackground() {
287
+ const canvas = document.querySelector('#ai-bg')
288
  if (!canvas) {
289
  throw new Error('canvas (#ai-bg) not found')
290
  }
 
291
  // const canvas = document.querySelector<HTMLCanvasElement>('#ai-bg')!
 
 
292
  const ctx = canvas.getContext('2d')
293
 
294
  canvas.width = canvas.offsetWidth
 
331
  }
332
 
333
 
334
+ async function startAIVoiceInterview(key, prompt) {
335
  const agent = new RealtimeAgent({
336
  name: 'Interviewer',
337
+ instructions: prompt ? prompt
338
+ : 'You are AI Agent currently interviewing candidates in Bahasa Indonesia. Ask everything related to their skill',
339
  });
340
+
341
  const session = new RealtimeSession(agent, {"model":"gpt-realtime"});
342
  try {
343
  await session.connect({
 
345
  // curl -s -X POST https://api.openai.com/v1/realtime/client_secrets -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" -d '{"session": {"type": "realtime", "model": "gpt-realtime"}}' | jq .value
346
  apiKey: key
347
  });
348
+ // console.log('You are connected!');
349
+
350
+ session.on('history_updated', (history) => {
351
+ // returns the full history of the session
352
+ // console.log(`HISTORY UPDATED`);
353
+ // console.log(history);
354
+ chat_history = history
355
+ });
356
+
357
+ // session.on('history_added', (history) => {
358
+ // console.log(`HISTORY ADDED`)
359
+ // console.log(history)
360
+ // })
361
  } catch (e) {
362
  console.error(e);
363
  }
src/style.css CHANGED
@@ -1,12 +1,12 @@
1
  * {
2
  box-sizing: border-box;
3
- font-family: system-ui, sans-serif;
4
  }
5
 
6
  body {
7
  margin: 0;
8
- background: #202124;
9
- color: white;
10
  }
11
 
12
  #app {
@@ -15,176 +15,227 @@ body {
15
  flex-direction: column;
16
  }
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  .top-bar {
19
- padding: 12px 20px;
20
- background: #303134;
 
21
  display: flex;
22
  justify-content: space-between;
23
  align-items: center;
 
24
  }
25
 
 
26
  .meeting-area {
27
  flex: 1;
28
  display: grid;
29
  grid-template-columns: 2fr 1fr;
30
- gap: 12px;
31
- padding: 12px;
32
  }
33
 
 
34
  .video-tile {
35
- background: #3c4043;
36
- border-radius: 12px;
37
  display: flex;
38
  align-items: center;
39
  justify-content: center;
40
  font-size: 1.2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
 
 
 
 
 
42
 
 
43
  .controls {
44
- padding: 12px;
45
- background: #303134;
 
46
  display: flex;
47
  justify-content: center;
48
- gap: 16px;
 
49
  }
50
 
 
51
  .control-btn {
52
- background: #3c4043;
53
  border: none;
54
  color: white;
55
  font-size: 1.1rem;
56
- padding: 12px 16px;
57
  border-radius: 50%;
58
  cursor: pointer;
 
59
  }
60
 
 
61
  .control-btn:hover {
62
- background: #5f6368;
 
 
63
  }
64
 
 
65
  .control-btn.end {
66
- border-radius: 24px;
67
- padding: 12px 20px;
68
- background: #ea4335;
69
- }
70
-
71
- .video-tile {
72
- position: relative;
73
- overflow: hidden;
74
  }
75
 
76
- /* video,
77
- canvas {
78
- width: 100%;
79
- height: 100%;
80
- object-fit: cover;
81
  }
82
 
83
- .label {
84
- position: absolute;
85
- bottom: 10px;
86
- left: 10px;
87
- background: rgba(0,0,0,0.5);
88
- padding: 6px 10px;
89
- border-radius: 8px;
90
- font-size: 0.9rem;
91
- } */
92
-
93
-
94
- /*
95
- :root {
96
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
97
- line-height: 1.5;
98
- font-weight: 400;
99
-
100
- color-scheme: light dark;
101
- color: rgba(255, 255, 255, 0.87);
102
- background-color: #242424;
103
-
104
- font-synthesis: none;
105
- text-rendering: optimizeLegibility;
106
- -webkit-font-smoothing: antialiased;
107
- -moz-osx-font-smoothing: grayscale;
108
  }
109
 
110
- a {
111
- font-weight: 500;
112
- color: #646cff;
113
- text-decoration: inherit;
114
- }
115
- a:hover {
116
- color: #535bf2;
117
  }
118
 
119
  body {
120
  margin: 0;
121
- display: flex;
122
- place-items: center;
123
- min-width: 320px;
124
- min-height: 100vh;
125
- }
126
-
127
- h1 {
128
- font-size: 3.2em;
129
- line-height: 1.1;
130
  }
131
 
132
  #app {
133
- max-width: 1280px;
134
- margin: 0 auto;
135
- padding: 2rem;
136
- text-align: center;
137
  }
138
 
139
- .logo {
140
- height: 6em;
141
- padding: 1.5em;
142
- will-change: filter;
143
- transition: filter 300ms;
144
- }
145
- .logo:hover {
146
- filter: drop-shadow(0 0 2em #646cffaa);
147
  }
148
- .logo.vanilla:hover {
149
- filter: drop-shadow(0 0 2em #3178c6aa);
 
 
 
 
 
150
  }
151
 
152
- .card {
153
- padding: 2em;
 
 
 
 
 
154
  }
155
 
156
- .read-the-docs {
157
- color: #888;
 
 
 
 
158
  }
159
 
160
- button {
161
- border-radius: 8px;
162
- border: 1px solid transparent;
163
- padding: 0.6em 1.2em;
164
- font-size: 1em;
165
- font-weight: 500;
166
- font-family: inherit;
167
- background-color: #1a1a1a;
168
  cursor: pointer;
169
- transition: border-color 0.25s;
170
  }
171
- button:hover {
172
- border-color: #646cff;
 
173
  }
174
- button:focus,
175
- button:focus-visible {
176
- outline: 4px auto -webkit-focus-ring-color;
 
 
177
  }
178
 
179
- @media (prefers-color-scheme: light) {
180
- :root {
181
- color: #213547;
182
- background-color: #ffffff;
183
- }
184
- a:hover {
185
- color: #747bff;
186
- }
187
- button {
188
- background-color: #f9f9f9;
189
- }
190
- } */
 
1
  * {
2
  box-sizing: border-box;
3
+ font-family: 'Poppins', system-ui, sans-serif;
4
  }
5
 
6
  body {
7
  margin: 0;
8
+ background: linear-gradient(135deg, #3B82F6, #6366F1);
9
+ color: #ffffff;
10
  }
11
 
12
  #app {
 
15
  flex-direction: column;
16
  }
17
 
18
+ .status-container {
19
+ display: flex; /* Makes items sit side-by-side */
20
+ align-items: center; /* Aligns them vertically in the middle */
21
+ gap: 15px; /* Adds space between the logo and text */
22
+ padding: 20px;
23
+ border: 1px solid #eee;
24
+ border-radius: 8px;
25
+ }
26
+
27
+ .status-logo {
28
+ height: 50px; /* Adjust size as needed */
29
+ width: auto;
30
+ }
31
+
32
+ /* Top Bar */
33
  .top-bar {
34
+ padding: 14px 24px;
35
+ background: rgba(255, 255, 255, 0.08);
36
+ backdrop-filter: blur(14px);
37
  display: flex;
38
  justify-content: space-between;
39
  align-items: center;
40
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
41
  }
42
 
43
+ /* Meeting Area */
44
  .meeting-area {
45
  flex: 1;
46
  display: grid;
47
  grid-template-columns: 2fr 1fr;
48
+ gap: 20px;
49
+ padding: 24px;
50
  }
51
 
52
+ /* Video Tiles */
53
  .video-tile {
54
+ background: rgba(255, 255, 255, 0.12);
55
+ border-radius: 20px;
56
  display: flex;
57
  align-items: center;
58
  justify-content: center;
59
  font-size: 1.2rem;
60
+ backdrop-filter: blur(10px);
61
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
62
+ transition: all 0.3s ease;
63
+ position: relative;
64
+ overflow: hidden;
65
+ }
66
+
67
+
68
+ .meeting-title {
69
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
70
+ font-size: 1.5rem;
71
+ color: #ffffff;
72
+ margin: 0; /* Removes default H1 spacing */
73
+ letter-spacing: -1px; /* Modern, tighter look */
74
+ }
75
+
76
+ .meeting-time {
77
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
78
+ font-size: 2.5rem;
79
+ color: #ffffff;
80
+ margin: 0; /* Removes default H1 spacing */
81
+ letter-spacing: -1px; /* Modern, tighter look */
82
+ padding-right: 1%;
83
+ }
84
+
85
+ .label {
86
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
87
+ font-size: 2.5rem;
88
+ color: #ffffff;
89
+ margin: 0; /* Removes default H1 spacing */
90
+ letter-spacing: -1px; /* Modern, tighter look */
91
+ }
92
+
93
+ /* Subtle brand border */
94
+ .video-tile::before {
95
+ content: "";
96
+ position: absolute;
97
+ inset: 0;
98
+ border-radius: 20px;
99
+ padding: 2px;
100
+ justify-content: center;
101
+ background: linear-gradient(135deg, #F97316, #3B82F6);
102
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
103
+ -webkit-mask-composite: xor;
104
+ mask-composite: exclude;
105
+ }
106
+
107
+ .video-tile:hover {
108
+ transform: translateY(-5px);
109
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
110
  }
111
+ /*
112
+ .video-title.user {
113
+ justify-content: center;
114
+ align-items: center;
115
+ } */
116
 
117
+ /* Controls */
118
  .controls {
119
+ padding: 18px;
120
+ background: rgba(255, 255, 255, 0.08);
121
+ backdrop-filter: blur(14px);
122
  display: flex;
123
  justify-content: center;
124
+ gap: 22px;
125
+ border-top: 1px solid rgba(255, 255, 255, 0.15);
126
  }
127
 
128
+ /* Control Buttons */
129
  .control-btn {
130
+ background: rgba(255, 255, 255, 0.15);
131
  border: none;
132
  color: white;
133
  font-size: 1.1rem;
134
+ padding: 15px;
135
  border-radius: 50%;
136
  cursor: pointer;
137
+ transition: all 0.25s ease;
138
  }
139
 
140
+ /* Orange hover accent */
141
  .control-btn:hover {
142
+ background: #F97316;
143
+ transform: scale(1.1);
144
+ box-shadow: 0 8px 20px rgba(249, 115, 22, 0.5);
145
  }
146
 
147
+ /* End Button */
148
  .control-btn.end {
149
+ border-radius: 28px;
150
+ padding: 14px 28px;
151
+ background: linear-gradient(135deg, #F97316, #FB923C);
152
+ font-weight: 600;
153
+ color: white;
 
 
 
154
  }
155
 
156
+ .control-btn.end:hover {
157
+ box-shadow: 0 10px 25px rgba(249, 115, 22, 0.6);
158
+ transform: scale(1.05);
 
 
159
  }
160
 
161
+ /* Responsive */
162
+ @media (max-width: 900px) {
163
+ .meeting-area {
164
+ grid-template-columns: 1fr;
165
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  }
167
 
168
+ /* * {
169
+ box-sizing: border-box;
170
+ font-family: system-ui, sans-serif;
 
 
 
 
171
  }
172
 
173
  body {
174
  margin: 0;
175
+ background: #202124;
176
+ color: white;
 
 
 
 
 
 
 
177
  }
178
 
179
  #app {
180
+ height: 100vh;
181
+ display: flex;
182
+ flex-direction: column;
 
183
  }
184
 
185
+ .top-bar {
186
+ padding: 12px 20px;
187
+ background: #303134;
188
+ display: flex;
189
+ justify-content: space-between;
190
+ align-items: center;
 
 
191
  }
192
+
193
+ .meeting-area {
194
+ flex: 1;
195
+ display: grid;
196
+ grid-template-columns: 2fr 1fr;
197
+ gap: 12px;
198
+ padding: 12px;
199
  }
200
 
201
+ .video-tile {
202
+ background: #3c4043;
203
+ border-radius: 12px;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ font-size: 1.2rem;
208
  }
209
 
210
+ .controls {
211
+ padding: 12px;
212
+ background: #303134;
213
+ display: flex;
214
+ justify-content: center;
215
+ gap: 16px;
216
  }
217
 
218
+ .control-btn {
219
+ background: #3c4043;
220
+ border: none;
221
+ color: white;
222
+ font-size: 1.1rem;
223
+ padding: 12px 16px;
224
+ border-radius: 50%;
 
225
  cursor: pointer;
 
226
  }
227
+
228
+ .control-btn:hover {
229
+ background: #5f6368;
230
  }
231
+
232
+ .control-btn.end {
233
+ border-radius: 24px;
234
+ padding: 12px 20px;
235
+ background: #ea4335;
236
  }
237
 
238
+ .video-tile {
239
+ position: relative;
240
+ overflow: hidden;
241
+ } */
 
 
 
 
 
 
 
 
vite.config.ts CHANGED
@@ -1,8 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { defineConfig } from 'vite'
 
 
2
 
3
  export default defineConfig({
4
- build: {
5
- outDir: 'dist',
6
- emptyOutDir: true
7
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  })
 
1
+ // // import { defineConfig } from 'vite'
2
+
3
+ // // export default defineConfig({
4
+ // // build: {
5
+ // // outDir: 'dist',
6
+ // // emptyOutDir: true
7
+ // // }
8
+ // // })
9
+
10
+ // import { defineConfig } from 'vite'
11
+ // import fs from 'fs'
12
+ // import path from 'path'
13
+
14
+ // export default defineConfig({
15
+ // plugins: [
16
+ // {
17
+ // name: 'custom-router',
18
+ // configureServer(server) {
19
+ // server.middlewares.use((req, res, next) => {
20
+
21
+ // // Only allow /interview/*
22
+ // if (req.url!.startsWith('/interview/')) {
23
+ // const filePath = path.resolve('index.html')
24
+ // const html = fs.readFileSync(filePath, 'utf-8')
25
+
26
+ // res.setHeader('Content-Type', 'text/html')
27
+ // res.statusCode = 200
28
+ // res.end(html)
29
+ // return
30
+ // }
31
+
32
+ // // Everything else → 404
33
+ // res.statusCode = 404
34
+ // res.setHeader('Content-Type', 'text/plain')
35
+ // res.end('404 Not Found')
36
+ // })
37
+ // }
38
+ // }
39
+ // ]
40
+ // })
41
+
42
+
43
  import { defineConfig } from 'vite'
44
+ import fs from 'fs'
45
+ import path from 'path'
46
 
47
  export default defineConfig({
48
+ server: {
49
+ proxy: {
50
+ '/interview': {
51
+ target: 'http://127.0.0.1:8000',
52
+ changeOrigin: true,
53
+ rewrite: (path) => path, // keep the path as-is
54
+ }
55
+ }
56
+ },
57
+ plugins: [
58
+ {
59
+ name: 'custom-router',
60
+ configureServer(server) {
61
+ server.middlewares.use((req, res, next) => {
62
+ const url = req.url || ''
63
+
64
+ // ✅ Allow Vite dev server internal requests
65
+ if (
66
+ url.startsWith('/@vite/') || // Vite HMR & modules
67
+ url.startsWith('/src/') || // src files
68
+ url.startsWith('/node_modules/') ||
69
+ url.startsWith('/public/') ||
70
+ url.includes('.') // any file with extension
71
+ ) {
72
+ return next()
73
+ }
74
+
75
+ // ✅ Serve interview.html only for /interview/*
76
+ if (url.startsWith('/interview/')) {
77
+ const filePath = path.resolve('index.html')
78
+ const html = fs.readFileSync(filePath, 'utf-8')
79
+
80
+ res.statusCode = 200
81
+ res.setHeader('Content-Type', 'text/html')
82
+ res.end(html)
83
+ return
84
+ }
85
+
86
+ // ❌ Everything else → 404
87
+ res.statusCode = 404
88
+ res.setHeader('Content-Type', 'text/plain')
89
+ res.end('404 Not Found')
90
+ })
91
+ }
92
+ }
93
+ ]
94
  })