chat-bot / index.html
kanhaiyaML's picture
Update index.html
44f74b9 verified
Raw
History Blame Contribute Delete
72 kB
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js">
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KAI — Your AI Assistant</title>
<link
href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700&family=DM+Mono:wght@300;400;500&display=swap"
rel="stylesheet">
<style>
:root {
--bg: #0d0d0f;
--sidebar-bg: #111114;
--surface: #18181c;
--surface2: #1f1f24;
--border: #2a2a30;
--accent: #7c6aff;
--accent2: #a78bfa;
--text: #e8e8f0;
--text-muted: #6b6b7a;
--text-dim: #3a3a45;
--user-bubble: #1e1b38;
--ai-bubble: #18181c;
--danger: #ff5f5f;
--success: #4ade80;
--green: #3fb950;
--green-bg: rgba(63, 185, 80, 0.08);
--red: #f85149;
--red-bg: rgba(248, 81, 73, 0.08);
--orange: #d2991d;
--orange-bg: rgba(210, 153, 29, 0.08);
--purple: #a371f7;
--purple-bg: rgba(163, 113, 247, 0.08);
--radius: 12px;
--radius-sm: 8px;
--transition: 0.2s ease;
--font-mono: 'DM Mono', monospace;
--font-sans: 'Syne', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background: var(--bg);
color: var(--text);
height: 100vh;
display: flex;
overflow: hidden;
}
/* SIDEBAR */
.sidebar {
width: 260px;
min-width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 16px 12px;
gap: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
z-index: 10;
}
.sidebar.collapsed {
width: 0;
min-width: 0;
padding: 16px 0;
overflow: hidden;
border-right: none;
}
.sidebar-toggle {
position: fixed;
top: 16px;
left: 248px;
right: -12px;
width: 28px;
height: 29px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 30;
transition: all 0.3s ease;
color: var(--text-muted);
font-size: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.sidebar-toggle:hover {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.sidebar.collapsed + .sidebar-toggle,
.sidebar-toggle.collapsed {
left: 10px;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 8px 16px;
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
min-width: 200px;
}
.logo {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.05em;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
white-space: nowrap;
}
/* Mode Switcher */
.mode-switcher {
display: flex;
gap: 4px;
padding: 4px;
background: var(--surface);
border-radius: 10px;
margin-bottom: 4px;
min-width: 200px;
}
.mode-btn {
flex: 1;
padding: 8px 12px;
border-radius: 8px;
border: none;
background: transparent;
color: var(--text-muted);
font-family: var(--font-sans);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
white-space: nowrap;
}
.mode-btn.active {
background: var(--surface2);
color: var(--text);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.mode-btn:hover:not(.active) {
color: var(--text);
background: rgba(255, 255, 255, 0.03);
}
.new-chat-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-family: var(--font-sans);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
width: 100%;
text-align: left;
min-width: 200px;
}
.new-chat-btn:hover {
background: var(--surface2);
border-color: var(--accent);
color: var(--accent2);
}
.new-chat-btn .icon {
width: 20px;
height: 20px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.conversations-label {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
color: var(--text-muted);
padding: 8px 8px 4px;
text-transform: uppercase;
min-width: 200px;
white-space: nowrap;
}
.conversations-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 200px;
}
.conversations-list::-webkit-scrollbar {
width: 3px;
}
.conversations-list::-webkit-scrollbar-track {
background: transparent;
}
.conversations-list::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
.conv-item {
padding: 9px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.15s;
border: 1px solid transparent;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.conv-item:hover {
background: var(--surface);
color: var(--text);
}
.conv-item.active {
background: var(--surface2);
color: var(--text);
border-color: var(--border);
}
.conv-item.active::before {
content: '';
position: absolute;
left: 0;
top: 20%;
height: 60%;
width: 2px;
background: var(--accent);
border-radius: 0 2px 2px 0;
}
.conv-item .conv-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-delete-btn {
width: 20px;
height: 20px;
border-radius: 4px;
border: none;
background: transparent;
color: var(--text-dim);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.15s;
flex-shrink: 0;
opacity: 0;
line-height: 1;
}
.conv-item:hover .conv-delete-btn {
opacity: 1;
}
.conv-delete-btn:hover {
background: var(--red-bg);
color: var(--red);
}
/* MAIN */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.top-bar {
padding: 16px 24px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
background: var(--bg);
min-height: 60px;
}
.top-bar .conv-title {
font-size: 14px;
font-weight: 500;
color: var(--text-muted);
}
.status-dot {
width: 7px;
height: 7px;
background: var(--success);
border-radius: 50%;
box-shadow: 0 0 6px var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* CHAT MODE */
.chat-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.messages-area {
flex: 1;
overflow-y: auto;
padding: 24px 0;
display: flex;
flex-direction: column;
gap: 0;
}
.messages-area::-webkit-scrollbar {
width: 4px;
}
.messages-area::-webkit-scrollbar-track {
background: transparent;
}
.messages-area::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
.welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 16px;
padding: 60px 24px;
text-align: center;
animation: fadeIn 0.5s ease;
}
.welcome-logo {
font-size: 48px;
font-weight: 700;
background: linear-gradient(135deg, var(--accent), var(--accent2), #c4b5fd);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.welcome p {
color: var(--text-muted);
font-size: 15px;
max-width: 340px;
line-height: 1.6;
}
.message-row {
display: flex;
padding: 12px 24px;
gap: 14px;
animation: slideUp 0.25s ease;
align-items: flex-start;
justify-content: center;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.message-row:hover {
background: rgba(255, 255, 255, 0.01);
}
.avatar {
width: 30px;
height: 30px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
margin-top: 2px;
}
.avatar.user {
background: linear-gradient(135deg, #3b2fa0, #6d5be0);
color: white;
}
.avatar.ai {
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: white;
}
.message-content {
flex: 1;
max-width: 720px;
}
.message-sender {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 6px;
color: var(--text-muted);
}
.message-text {
font-family: var(--font-mono);
font-size: 14px;
line-height: 1.7;
color: var(--text);
white-space: pre-wrap;
word-break: break-word;
}
.message-text.user-text {
font-family: var(--font-sans);
font-size: 15px;
}
.message-text pre {
background: #0d0d0f;
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
overflow-x: auto;
margin: 8px 0;
}
.message-text code {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent2);
}
.message-text pre code {
color: var(--text);
}
.typing-indicator {
display: flex;
gap: 4px;
align-items: center;
padding: 4px 0;
}
.typing-indicator span {
width: 6px;
height: 6px;
background: var(--accent);
border-radius: 50%;
animation: bounce 1.2s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%,
60%,
100% {
transform: translateY(0);
opacity: 0.4;
}
30% {
transform: translateY(-5px);
opacity: 1;
}
}
/* CHAT INPUT */
.input-area {
padding: 16px 24px 20px;
background: var(--bg);
border-top: 1px solid var(--border);
}
.input-wrapper {
display: flex;
align-items: flex-end;
gap: 10px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 12px 14px;
transition: border-color 0.2s;
max-width: 860px;
margin: 0 auto;
}
.input-wrapper:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(124, 106, 255, 0.1);
}
#messageInput {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text);
font-family: var(--font-sans);
font-size: 14px;
resize: none;
max-height: 160px;
min-height: 24px;
line-height: 1.5;
}
#messageInput::placeholder {
color: var(--text-muted);
}
#sendBtn {
width: 34px;
height: 34px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border: none;
border-radius: 9px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
opacity: 0.5;
}
#sendBtn.active {
opacity: 1;
transform: scale(1.05);
}
#sendBtn:hover.active {
transform: scale(1.1);
}
#sendBtn svg {
width: 16px;
height: 16px;
fill: white;
}
.input-hint {
text-align: center;
font-size: 11px;
color: var(--text-dim);
margin-top: 10px;
font-family: var(--font-mono);
}
.toggles {
display: flex;
gap: 8px;
max-width: 860px;
margin: 0 auto 10px;
}
.toggle-btn {
padding: 6px 14px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-muted);
font-family: var(--font-sans);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn.on {
background: rgba(124, 106, 255, 0.15);
border-color: var(--accent);
color: var(--accent2);
}
/* CODE REVIEW MODE */
.review-container {
display: none;
flex: 1;
flex-direction: column;
overflow-y: auto;
padding: 24px;
gap: 24px;
}
.review-container::-webkit-scrollbar {
width: 4px;
}
.review-container::-webkit-scrollbar-track {
background: transparent;
}
.review-container::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
.review-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
transition: border-color var(--transition);
}
.review-card:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(124, 106, 255, 0.1);
}
.card-label {
display: block;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 8px;
}
.review-textarea {
width: 100%;
min-height: 200px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-family: var(--font-mono);
font-size: 0.9rem;
padding: 16px;
resize: vertical;
outline: none;
transition: border-color var(--transition);
line-height: 1.55;
tab-size: 4;
}
.review-textarea::placeholder {
color: #484f58;
}
.review-textarea:focus {
border-color: var(--accent);
}
.review-input-row {
display: flex;
gap: 12px;
margin-top: 12px;
flex-wrap: wrap;
}
.review-question-input {
flex: 1;
min-width: 200px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-family: var(--font-sans);
font-size: 0.9rem;
padding: 12px 16px;
outline: none;
transition: border-color var(--transition);
}
.review-question-input::placeholder {
color: #484f58;
}
.review-question-input:focus {
border-color: var(--accent);
}
.review-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius-sm);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
letter-spacing: 0.01em;
transition: all var(--transition);
white-space: nowrap;
font-family: var(--font-sans);
position: relative;
overflow: hidden;
}
.review-btn:hover {
background: var(--accent2);
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(124, 106, 255, 0.3);
}
.review-btn:active {
transform: translateY(0);
box-shadow: none;
}
.review-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
pointer-events: none;
}
.review-btn .spinner {
display: none;
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
.review-btn.loading .spinner {
display: block;
}
.review-btn.loading .btn-text {
display: none;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Review Output */
.review-output-area {
display: flex;
flex-direction: column;
gap: 16px;
}
.output-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
animation: fadeSlideIn 0.35s ease forwards;
opacity: 0;
transform: translateY(12px);
}
@keyframes fadeSlideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.output-card:nth-child(1) {
animation-delay: 0s;
}
.output-card:nth-child(2) {
animation-delay: 0.06s;
}
.output-card:nth-child(3) {
animation-delay: 0.12s;
}
.output-card:nth-child(4) {
animation-delay: 0.18s;
}
.output-card:nth-child(5) {
animation-delay: 0.24s;
}
.output-card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
font-weight: 600;
font-size: 0.95rem;
letter-spacing: 0.01em;
}
.output-card-body {
padding: 16px 20px;
}
.output-card-body pre {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 16px;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.85rem;
line-height: 1.6;
color: #d2a8ff;
white-space: pre-wrap;
word-break: break-word;
position: relative;
}
.output-card-body ul {
list-style: none;
padding: 0;
}
.output-card-body ul li {
padding: 8px 12px;
margin: 4px 0;
background: var(--surface2);
border-radius: 6px;
font-size: 0.9rem;
border-left: 3px solid var(--border);
line-height: 1.5;
}
.output-card-body p {
font-size: 0.9rem;
color: var(--text-muted);
line-height: 1.6;
}
/* Color accents per card type */
.card-bugs .output-card-header {
color: var(--red);
background: var(--red-bg);
}
.card-bugs ul li {
border-left-color: var(--red);
}
.card-security .output-card-header {
color: var(--orange);
background: var(--orange-bg);
}
.card-security ul li {
border-left-color: var(--orange);
}
.card-quality .output-card-header {
color: var(--accent);
background: rgba(124, 106, 255, 0.06);
}
.card-suggestions .output-card-header {
color: var(--purple);
background: var(--purple-bg);
}
.card-suggestions ul li {
border-left-color: var(--purple);
}
.card-refactored .output-card-header {
color: var(--green);
background: var(--green-bg);
}
/* Quality Score */
.score-display {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.score-circle {
position: relative;
width: 80px;
height: 80px;
flex-shrink: 0;
}
.score-circle svg {
transform: rotate(-90deg);
}
.score-circle .bg-ring {
fill: none;
stroke: var(--border);
stroke-width: 8;
}
.score-circle .fg-ring {
fill: none;
stroke: var(--accent);
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dashoffset 0.8s ease;
}
.score-circle .score-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
color: var(--text);
}
.score-label {
font-size: 0.85rem;
color: var(--text-muted);
}
.score-high .fg-ring {
stroke: var(--green);
}
.score-mid .fg-ring {
stroke: var(--orange);
}
.score-low .fg-ring {
stroke: var(--red);
}
/* Copy Button */
.copy-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
transition: all var(--transition);
font-family: var(--font-sans);
margin-top: 10px;
}
.copy-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(124, 106, 255, 0.05);
}
.copy-btn.copied {
border-color: var(--green);
color: var(--green);
background: var(--green-bg);
}
.copy-btn svg {
width: 14px;
height: 14px;
}
/* Error banner */
.error-banner {
background: var(--red-bg);
border: 1px solid rgba(248, 81, 73, 0.3);
border-radius: var(--radius-sm);
padding: 14px 18px;
color: var(--red);
font-size: 0.9rem;
display: flex;
align-items: flex-start;
gap: 10px;
animation: fadeSlideIn 0.3s ease;
}
.error-banner svg {
width: 20px;
height: 20px;
flex-shrink: 0;
margin-top: 1px;
}
/* Empty state */
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.empty-state svg {
width: 56px;
height: 56px;
opacity: 0.4;
margin-bottom: 12px;
}
.empty-state p {
font-size: 0.9rem;
}
.empty-conv {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: var(--text-dim);
font-size: 12px;
font-family: var(--font-mono);
}
.hidden {
display: none !important;
}
/* Delete confirmation toast */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 20px;
color: var(--text);
font-size: 14px;
z-index: 1000;
animation: slideUp 0.3s ease;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.toast .toast-actions {
display: flex;
gap: 8px;
}
.toast button {
padding: 6px 14px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
cursor: pointer;
font-family: var(--font-sans);
font-size: 12px;
transition: all 0.2s;
}
.toast button.danger {
background: var(--red);
border-color: var(--red);
color: white;
}
.toast button:hover {
background: var(--surface2);
border-color: var(--accent);
}
.toast button.danger:hover {
background: #ff4444;
}
.scroll-bottom-btn {
position: fixed;
bottom: 150px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 40px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-muted);
font-size: 18px;
z-index: 25;
transition: all 0.2s;
opacity: 0;
pointer-events: none;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
.scroll-bottom-btn.visible {
opacity: 1;
pointer-events: auto;
}
.scroll-bottom-btn:hover {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.mic-btn {
width: 34px;
height: 34px;
background: transparent;
border: 1px solid var(--border);
border-radius: 9px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
color: var(--text-muted);
font-size: 16px;
}
.mic-btn.recording {
background: var(--red);
border-color: var(--red);
color: white;
animation: pulse 1.5s infinite;
}
.speak-btn {
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
padding: 4px 8px;
color: var(--text-muted);
font-size: 14px;
margin-top: 6px;
transition: all 0.2s;
}
.speak-btn:hover {
background: var(--surface2);
color: var(--accent);
}
</style>
</head>
<body>
<aside class="sidebar" id="sidebar">
<div class="sidebar-toggle" onclick="toggleSidebar()" title="Toggle Sidebar">
</div>
<div class="sidebar-header">
<span class="logo">KAI</span>
<div class="status-dot"></div>
</div>
<!-- Mode Switcher -->
<div class="mode-switcher">
<button class="mode-btn active" id="chatModeBtn" onclick="switchMode('chat')">
💬 Chat
</button>
<button class="mode-btn" id="reviewModeBtn" onclick="switchMode('review')">
🔍 Code Review
</button>
</div>
<button class="new-chat-btn" id="newChatBtn" onclick="startNewChat()">
<div class="icon">+</div>
New conversation
</button>
<div class="conversations-label">Recent</div>
<div class="conversations-list" id="convList"></div>
</aside>
<main class="main">
<div class="top-bar">
<div class="status-dot"></div>
<span class="conv-title" id="convTitle">Start a new conversation</span>
</div>
<!-- CHAT MODE -->
<div class="chat-container" id="chatContainer">
<div class="messages-area" id="messagesArea">
<div class="welcome" id="welcomeScreen">
<div class="welcome-logo">KAI</div>
<p>Your personal AI assistant. Ask me anything — I remember our conversations.</p>
</div>
</div>
<div class="input-area" id="chatInputArea">
<div class="toggles">
<button class="toggle-btn" id="webToggle" onclick="toggleFeature('web')">
🔍 Web Search
</button>
<button class="toggle-btn" id="deepToggle" onclick="toggleFeature('deep')">
🔬 Deep Research
</button>
</div>
<div class="input-wrapper">
<textarea id="messageInput" placeholder="Message KAI..." rows="1"
oninput="autoResize(this); toggleSend()"
onkeydown="handleKey(event)"></textarea>
<button class="mic-btn" id="micBtn" onclick="toggleRecording()" title="Voice input">
🎤
</button>
<button id="sendBtn" onclick="sendMessage()">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
<div class="input-hint">Enter to send · Shift+Enter for new line</div>
</div>
</div>
<!-- CODE REVIEW MODE -->
<div class="review-container" id="reviewContainer">
<div class="review-card" id="reviewInputCard">
<label class="card-label" for="codeInput">📋 Paste Your Code</label>
<textarea class="review-textarea" id="codeInput"
placeholder="Paste your code here...&#10;&#10;e.g.&#10;def calculate(a, b):&#10; return a + b"></textarea>
<div class="review-input-row">
<input type="text" class="review-question-input" id="reviewQuestionInput"
placeholder="💬 Optional: Ask a specific question about this code..." />
<button class="review-btn" id="reviewSubmitBtn" onclick="analyzeCode()">
<span class="btn-text">🔍 Analyze Code</span>
<span class="spinner"></span>
</button>
</div>
</div>
<div class="review-output-area" id="reviewOutputArea">
<div class="empty-state" id="reviewEmptyState">
<svg viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="12" width="40" height="32" rx="4" stroke="currentColor" stroke-width="2.5" fill="none"/>
<path d="M18 24h20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M18 30h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="40" cy="28" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M44 32l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<p>Paste your code above and click <strong>Analyze Code</strong><br>to see the review results here.</p>
</div>
<div class="error-banner" id="reviewErrorBanner" style="display:none;">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>
<span id="reviewErrorMessage"></span>
</div>
</div>
</div>
<button class="scroll-bottom-btn" id="scrollBottomBtn" onclick="scrollToBottom()" title="Scroll to bottom">
</button>
</main>
<script>
const API = 'https://kanhaiyaml-chat-bot.hf.space';
const REVIEW_API = 'https://kanhaiyaml-chat-bot.hf.space/review';
let currentConvId = null;
let isTyping = false;
let webSearchOn = false;
let deepResearchOn = false;
let currentMode = 'chat';
let sidebarCollapsed = false;
let conversationToDelete = null;
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;
async function toggleRecording() {
if (isRecording) {
await stopRecording();
} else {
await startRecording();
}
}
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = (e) => {
audioChunks.push(e.data);
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
await sendAudioToBackend(audioBlob);
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
isRecording = true;
document.getElementById('micBtn').classList.add('recording');
document.getElementById('micBtn').innerHTML = '🔴';
} catch (err) {
alert('Microphone access denied. Please allow microphone access.');
}
}
async function stopRecording() {
if (mediaRecorder) {
mediaRecorder.stop();
isRecording = false;
document.getElementById('micBtn').classList.remove('recording');
document.getElementById('micBtn').innerHTML = '🎤';
}
}
async function sendAudioToBackend(audioBlob) {
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
console.log("Sending audio, size:", audioBlob.size);
try {
const res = await fetch(`${API}/transcribe`, {
method: 'POST',
body: formData
});
console.log("Response status:", res.status);
if (!res.ok) {
const errorText = await res.text();
console.error("Transcription error:", errorText);
alert('Transcription failed: ' + errorText);
return;
}
const data = await res.json();
console.log("Transcription result:", data);
const transcribedText = data.text;
if (!transcribedText || transcribedText.trim() === '') {
alert('No text detected. Please try again.');
return;
}
document.getElementById('messageInput').value = transcribedText;
toggleSend();
autoResize(document.getElementById('messageInput'));
if (transcribedText.trim()) {
sendMessage();
}
} catch (err) {
console.error('Network error:', err);
alert('Failed to connect to server.');
}
}
// ── Sidebar Toggle ──────────────────────────────────
function toggleSidebar() {
sidebarCollapsed = !sidebarCollapsed;
const sidebar = document.getElementById('sidebar');
const toggleBtn = sidebar.querySelector('.sidebar-toggle');
if (sidebarCollapsed) {
sidebar.classList.add('collapsed');
toggleBtn.classList.add('collapsed');
toggleBtn.innerHTML = '▶';
} else {
sidebar.classList.remove('collapsed');
toggleBtn.classList.remove('collapsed');
toggleBtn.innerHTML = '◀';
}
}
// ── Mode Switching ──────────────────────────────────
function switchMode(mode) {
currentMode = mode;
const chatModeBtn = document.getElementById('chatModeBtn');
const reviewModeBtn = document.getElementById('reviewModeBtn');
const chatContainer = document.getElementById('chatContainer');
const reviewContainer = document.getElementById('reviewContainer');
const chatInputArea = document.getElementById('chatInputArea');
const newChatBtn = document.getElementById('newChatBtn');
if (mode === 'chat') {
chatModeBtn.classList.add('active');
reviewModeBtn.classList.remove('active');
chatContainer.style.display = 'flex';
chatInputArea.style.display = 'block';
reviewContainer.style.display = 'none';
newChatBtn.style.display = 'flex';
document.getElementById('convTitle').textContent = currentConvId ?
(document.querySelector(`.conv-item[data-id="${currentConvId}"]`)?.querySelector('.conv-title')?.textContent || 'New Chat') :
'Start a new conversation';
} else {
reviewModeBtn.classList.add('active');
chatModeBtn.classList.remove('active');
chatContainer.style.display = 'none';
chatInputArea.style.display = 'none';
reviewContainer.style.display = 'flex';
newChatBtn.style.display = 'none';
document.getElementById('convTitle').textContent = 'Code Review';
}
}
function toggleFeature(type) {
if (type === 'web') {
webSearchOn = !webSearchOn;
document.getElementById('webToggle').classList.toggle('on', webSearchOn);
} else {
deepResearchOn = !deepResearchOn;
document.getElementById('deepToggle').classList.toggle('on', deepResearchOn);
}
}
// ── Init ──────────────────────────────────────────────
async function init() {
await loadConversations();
switchMode('chat');
}
// ── Load sidebar conversations ────────────────────────
async function loadConversations() {
try {
const res = await fetch(`${API}/conversations`);
const convs = await res.json();
const list = document.getElementById('convList');
list.innerHTML = '';
if (convs.length === 0) {
list.innerHTML = '<div class="empty-conv">No conversations yet</div>';
return;
}
convs.forEach(c => {
const el = document.createElement('div');
el.className = 'conv-item' + (c.id === currentConvId ? ' active' : '');
el.dataset.id = c.id;
const titleSpan = document.createElement('span');
titleSpan.className = 'conv-title';
titleSpan.textContent = c.title || 'New Chat';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'conv-delete-btn';
deleteBtn.title = 'Delete conversation';
deleteBtn.innerHTML = '×';
deleteBtn.onclick = (e) => {
console.log("Delete button clicked");
e.stopPropagation();
confirmDeleteConversation(c.id, c.title || 'New Chat');
};
el.appendChild(titleSpan);
el.appendChild(deleteBtn);
el.onclick = () => {
if (currentMode !== 'chat') switchMode('chat');
loadConversation(c.id, c.title);
};
list.appendChild(el);
});
} catch (e) {
console.error('Failed to load conversations:', e);
}
}
// ── Confirm and delete conversation ──────────────────
function confirmDeleteConversation(id, title) {
console.log("confirmDelete called with id:", id, "title:", title);
conversationToDelete = { id, title };
// Remove existing toast if any
const existingToast = document.querySelector('.toast');
if (existingToast) existingToast.remove();
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = `
<span>Delete "${escapeHtml(title)}"?</span>
<div class="toast-actions">
<button onclick="cancelDelete()">Cancel</button>
<button class="danger" onclick="deleteConversation()">Delete</button>
</div>
`;
document.body.appendChild(toast);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (document.body.contains(toast)) {
toast.remove();
conversationToDelete = null;
}
}, 5000);
}
function cancelDelete() {
const toast = document.querySelector('.toast');
if (toast) toast.remove();
conversationToDelete = null;
}
async function deleteConversation() {
if (!conversationToDelete) return;
const { id } = conversationToDelete;
const toast = document.querySelector('.toast');
if (toast) toast.remove();
try {
const res = await fetch(`${API}/conversation/${id}`, {
method: 'DELETE',
});
if (!res.ok) {
throw new Error('Failed to delete conversation');
}
// If the deleted conversation was the current one, reset
if (currentConvId === id) {
currentConvId = null;
document.getElementById('messagesArea').innerHTML = `
<div class="welcome" id="welcomeScreen">
<div class="welcome-logo">KAI</div>
<p>Your personal AI assistant. Ask me anything — I remember our conversations.</p>
</div>
`;
document.getElementById('convTitle').textContent = 'Start a new conversation';
}
await loadConversations();
} catch (e) {
console.error('Failed to delete conversation:', e);
alert('Failed to delete conversation. Please try again.');
}
conversationToDelete = null;
}
// ── Load a conversation's messages ───────────────────
async function loadConversation(id, title) {
currentConvId = id;
document.getElementById('convTitle').textContent = title || 'New Chat';
const area = document.getElementById('messagesArea');
area.innerHTML = '';
try {
const res = await fetch(`${API}/conversation/${id}`);
const messages = await res.json();
if (messages.length === 0) {
area.innerHTML = '<div class="empty-conv">No messages yet — say something!</div>';
} else {
messages.forEach(m => appendMessage(m.role, m.content));
}
} catch (e) {
console.error('Failed to load conversation:', e);
}
await loadConversations();
scrollToBottom();
}
// ── Start new chat ────────────────────────────────────
async function startNewChat() {
if (currentMode !== 'chat') switchMode('chat');
try {
const res = await fetch(`${API}/conversation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'New Chat' })
});
const conv = await res.json();
currentConvId = conv.id;
document.getElementById('convTitle').textContent = 'New Chat';
document.getElementById('messagesArea').innerHTML =
'<div class="empty-conv">New conversation started — say something!</div>';
await loadConversations();
} catch (e) {
console.error('Failed to create conversation:', e);
}
}
// ── Send message ──────────────────────────────────────
async function sendMessage() {
const input = document.getElementById('messageInput');
const text = input.value;
if (!text || isTyping) return;
if (!currentConvId) {
await startNewChat();
}
const welcome = document.getElementById('welcomeScreen');
if (welcome) welcome.remove();
const emptyConv = document.querySelector('.empty-conv');
if (emptyConv) emptyConv.remove();
appendMessage('user', text);
input.value = '';
input.style.height = 'auto';
toggleSend();
showTyping();
isTyping = true;
try {
console.log("Fetching stream...", currentConvId);
const res = await fetch(`${API}/chat/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: currentConvId,
message: text,
web_search: webSearchOn,
deep_research: deepResearchOn
})
});
removeTyping();
// create empty assistant message to stream into
const msgRow = createEmptyAssistantMessage();
const msgEl = msgRow.querySelector('.message-text');
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
let streamDone = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
console.log("CHUNK:", chunk);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.toLowerCase().startsWith('data: ')) {
const token = line.slice(6);
if (token === '[DONE]') {
streamDone = true;
break;
}
fullText += token;
console.log("adding token:", token, "fullText now:", fullText.length);
msgEl.textContent = fullText;
await new Promise(resolve => setTimeout(resolve, 0));
scrollToBottom();
}
}
if (streamDone) break;
}
msgEl.innerHTML = marked.parse(fullText);
addSpeakButton(msgRow);
speakText(fullText);
msgEl.style.display = 'none';
msgEl.offsetHeight;
msgEl.style.display = '';
scrollToBottom();
await loadConversations();
} catch (e) {
removeTyping();
appendMessage('assistant', '⚠️ Something went wrong. Please try again.');
}
isTyping = false;
scrollToBottom();
await loadConversations();
}
// ── Append message to UI ──────────────────────────────
function appendMessage(role, content) {
const area = document.getElementById('messagesArea');
const row = document.createElement('div');
row.className = 'message-row';
const isUser = role === 'user';
row.innerHTML = `
<div class="avatar ${isUser ? 'user' : 'ai'}">${isUser ? 'U' : 'K'}</div>
<div class="message-content">
<div class="message-sender">${isUser ? 'You' : 'KAI'}</div>
<div class="message-text ${isUser ? 'user-text' : ''}">${isUser ? escapeHtml(content) : marked.parse(content)}</div>
</div>
`;
area.appendChild(row);
if (!isUser) addSpeakButton(row);
scrollToBottom();
}
function createEmptyAssistantMessage() {
const area = document.getElementById('messagesArea');
const row = document.createElement('div');
row.className = 'message-row';
row.innerHTML = `
<div class="avatar ai">K</div>
<div class="message-content">
<div class="message-sender">KAI</div>
<div class="message-text"></div>
</div>
`;
area.appendChild(row);
scrollToBottom();
return row;
}
// ── Typing indicator ──────────────────────────────────
function showTyping() {
const area = document.getElementById('messagesArea');
const row = document.createElement('div');
row.className = 'message-row';
row.id = 'typingRow';
row.innerHTML = `
<div class="avatar ai">K</div>
<div class="message-content">
<div class="message-sender">KAI</div>
<div class="typing-indicator"><span></span><span></span><span></span></div>
</div>
`;
area.appendChild(row);
scrollToBottom();
}
function removeTyping() {
const t = document.getElementById('typingRow');
if (t) t.remove();
}
// ── Helpers ───────────────────────────────────────────
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function scrollToBottom() {
const area = document.getElementById('messagesArea');
if (area) area.scrollTop = area.scrollHeight;
}
// Show/hide scroll-to-bottom button
const messagesArea = document.getElementById('messagesArea');
messagesArea.addEventListener('scroll', function() {
const btn = document.getElementById('scrollBottomBtn');
const isNearBottom = this.scrollHeight - this.scrollTop - this.clientHeight < 100;
if (isNearBottom) {
btn.classList.remove('visible');
} else {
btn.classList.add('visible');
}
});
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 160) + 'px';
}
function toggleSend() {
const val = document.getElementById('messageInput').value.trim();
const btn = document.getElementById('sendBtn');
btn.classList.toggle('active', val.length > 0);
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
// function speakText(text) {
// if ('speechSynthesis' in window) {
// window.speechSynthesis.cancel();
// const utterance = new SpeechSynthesisUtterance(text);
// utterance.lang = 'hi-IN'
// utterance.rate = 1;
// utterance.pitch = 1;
// window.speechSynthesis.speak(utterance);
// }
// }
function addSpeakButton(row) {
const msgText = row.querySelector('.message-text');
const text = msgText.textContent;
const speakBtn = document.createElement('button');
speakBtn.className = 'speak-btn';
speakBtn.innerHTML = '🔊';
speakBtn.title = 'Listen';
speakBtn.onclick = () => speakText(text);
row.querySelector('.message-content').appendChild(speakBtn);
}
async function speakText(text) {
try {
const res = await fetch(`${API}/speak`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text })
});
const audioBlob = await res.blob();
// Check if blob is valid
if (audioBlob.size < 100) {
console.error("Audio too small, something wrong");
return;
}
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
// Must be triggered by user click - already is since speakText is called from onclick
audio.load();
await audio.play();
} catch (err) {
console.error('Failed to speak:', err);
// Fallback to browser TTS
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'hi-IN';
window.speechSynthesis.speak(utterance);
}
}
}
// ── CODE REVIEW LOGIC ─────────────────────────────────
const codeInput = document.getElementById('codeInput');
const reviewQuestionInput = document.getElementById('reviewQuestionInput');
const reviewSubmitBtn = document.getElementById('reviewSubmitBtn');
const reviewOutputArea = document.getElementById('reviewOutputArea');
const reviewEmptyState = document.getElementById('reviewEmptyState');
const reviewErrorBanner = document.getElementById('reviewErrorBanner');
const reviewErrorMessage = document.getElementById('reviewErrorMessage');
codeInput.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
analyzeCode();
}
});
reviewQuestionInput.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
analyzeCode();
}
});
async function analyzeCode() {
const code = codeInput.value.trim();
const question = reviewQuestionInput.value.trim();
if (!code) {
showReviewError('Please paste some code before analyzing.');
codeInput.focus();
return;
}
clearReviewOutput();
hideReviewError();
setReviewLoading(true);
reviewEmptyState.style.display = 'none';
try {
const response = await fetch(REVIEW_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
code: code,
question: question || undefined,
}),
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Server responded with ${response.status} ${response.statusText}. ${errorText}`);
}
const data = await response.json();
renderReviewOutput(data);
} catch (err) {
console.error('Analysis error:', err);
if (err.message.includes('Failed to fetch') || err.message.includes('NetworkError')) {
showReviewError('⚠️ Could not connect to the backend at ' + REVIEW_API +
'. Make sure your server is running and CORS is enabled.');
} else {
showReviewError(err.message || 'An unexpected error occurred.');
}
reviewEmptyState.style.display = 'none';
} finally {
setReviewLoading(false);
}
}
function renderReviewOutput(data) {
const existingCards = reviewOutputArea.querySelectorAll('.output-card');
existingCards.forEach(card => card.remove());
const fragments = [];
if (data.bugs !== undefined && data.bugs !== null) {
const bugsList = Array.isArray(data.bugs) ? data.bugs : [data.bugs];
fragments.push(createReviewOutputCard({
type: 'bugs',
title: '🐛 Bugs',
bodyHTML: bugsList.length > 0 ?
'<ul>' + bugsList.map(b => `<li>${escapeHtml(String(b))}</li>`).join('') + '</ul>' :
'<p>✅ No bugs detected.</p>',
}));
}
if (data.security_issues !== undefined && data.security_issues !== null) {
const secList = Array.isArray(data.security_issues) ? data.security_issues : [data.security_issues];
fragments.push(createReviewOutputCard({
type: 'security',
title: '🔒 Security Issues',
bodyHTML: secList.length > 0 ?
'<ul>' + secList.map(s => `<li>${escapeHtml(String(s))}</li>`).join('') + '</ul>' :
'<p>✅ No security issues found.</p>',
}));
}
if (data.quality_score !== undefined && data.quality_score !== null) {
const score = parseFloat(data.quality_score);
const scoreNum = isNaN(score) ? 0 : Math.max(0, Math.min(100, score));
const scoreClass = scoreNum >= 70 ? 'score-high' : scoreNum >= 40 ? 'score-mid' : 'score-low';
const circumference = 2 * Math.PI * 32;
const offset = circumference - (scoreNum / 100) * circumference;
fragments.push(createReviewOutputCard({
type: 'quality',
title: '📊 Quality Score',
bodyHTML: `
<div class="score-display">
<div class="score-circle ${scoreClass}">
<svg width="80" height="80" viewBox="0 0 80 80">
<circle class="bg-ring" cx="40" cy="40" r="32"/>
<circle class="fg-ring" cx="40" cy="40" r="32"
stroke-dasharray="${circumference}"
stroke-dashoffset="${offset}"/>
</svg>
<span class="score-text">${Math.round(scoreNum)}</span>
</div>
<div>
<span class="score-label">out of 100</span>
<p style="margin-top:4px;">${scoreNum >= 70 ? 'Great quality!' : scoreNum >= 40 ? 'Needs some improvement.' : 'Significant issues found.'}</p>
</div>
</div>`,
}));
}
if (data.suggestions !== undefined && data.suggestions !== null) {
const sugList = Array.isArray(data.suggestions) ? data.suggestions : [data.suggestions];
fragments.push(createReviewOutputCard({
type: 'suggestions',
title: '💡 Suggestions',
bodyHTML: sugList.length > 0 ?
'<ul>' + sugList.map(s => `<li>${escapeHtml(String(s))}</li>`).join('') + '</ul>' :
'<p>No specific suggestions — code looks good!</p>',
}));
}
if (data.refactored_code !== undefined && data.refactored_code !== null) {
const refactoredCode = String(data.refactored_code);
const cardEl = createReviewOutputCard({
type: 'refactored',
title: '✨ Refactored Code',
bodyHTML: `
<pre><code>${escapeHtml(refactoredCode)}</code></pre>
<button class="copy-btn" onclick="copyRefactoredCode(this, '${escapeAttr(refactoredCode)}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
Copy Code
</button>`,
});
fragments.push(cardEl);
}
if (fragments.length === 0) {
fragments.push(createReviewOutputCard({
type: 'refactored',
title: '📦 Raw Response',
bodyHTML: `<pre><code>${escapeHtml(JSON.stringify(data, null, 2))}</code></pre>`,
}));
}
fragments.forEach(frag => reviewOutputArea.appendChild(frag));
}
function createReviewOutputCard({ type, title, bodyHTML }) {
const card = document.createElement('div');
card.className = `output-card card-${type}`;
card.innerHTML = `
<div class="output-card-header">
<span>${title}</span>
</div>
<div class="output-card-body">
${bodyHTML}
</div>`;
return card;
}
function copyRefactoredCode(btn, codeText) {
navigator.clipboard.writeText(codeText).then(() => {
btn.classList.add('copied');
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
Copied!`;
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
Copy Code`;
}, 2000);
}).catch(() => {
const textarea = document.createElement('textarea');
textarea.value = codeText;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
btn.classList.add('copied');
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
Copied!`;
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
Copy Code`;
}, 2000);
});
}
function clearReviewOutput() {
const existingCards = reviewOutputArea.querySelectorAll('.output-card');
existingCards.forEach(card => card.remove());
reviewEmptyState.style.display = 'none';
}
function showReviewError(msg) {
reviewErrorMessage.textContent = msg;
reviewErrorBanner.style.display = 'flex';
}
function hideReviewError() {
reviewErrorBanner.style.display = 'none';
reviewErrorMessage.textContent = '';
}
function setReviewLoading(isLoading) {
if (isLoading) {
reviewSubmitBtn.classList.add('loading');
reviewSubmitBtn.disabled = true;
} else {
reviewSubmitBtn.classList.remove('loading');
reviewSubmitBtn.disabled = false;
}
}
function escapeAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g,
'&#39;');
}
init();
</script>
</body>
</html>