Skip to content

Commit 333f845

Browse files
committed
Clarify copilot consent and add responsive fullscreen controls
1 parent 403e8c3 commit 333f845

9 files changed

Lines changed: 196 additions & 79 deletions

File tree

blueprints/lms/src/pages/lms.rs

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -765,21 +765,22 @@ fn render_lms_ai_widget(csrf_token: &str) -> String {
765765
</div>
766766
</div>
767767
768-
<div id="lms-ai-drawer" class="lms-ai-drawer" style="display: none;" role="dialog" aria-label="Academic Copilot">
768+
<div id="lms-ai-drawer" data-copilot data-copilot-launcher="lms-crab-launcher" data-copilot-backdrop="" class="lms-ai-drawer" style="display: none;" role="dialog" aria-label="Academic Copilot">
769769
<div class="lms-drawer-header">
770770
<div style="display: flex; align-items: center; gap: 10px;">
771771
<div style="width: 34px; height: 34px; border-radius: 10px; background: rgba(52, 211, 153, 0.15); border: 1px solid rgba(52, 211, 153, 0.3); display: flex; align-items: center; justify-content: center; font-size: 1.1rem;">🎓</div>
772772
<div>
773773
<div style="font-weight: 800; font-size: 0.95rem; color: #fff;">Academic Copilot</div>
774-
<div style="font-size: 0.72rem; color: #34d399; display: flex; align-items: center; gap: 5px;">
774+
<div class="copilot-subtitle" style="font-size: 0.72rem; color: #34d399; display: flex; align-items: center; gap: 5px;">
775775
<span style="width: 6px; height: 6px; border-radius: 50%; background: #34d399; display: inline-block;"></span>
776776
<span>AI Learning & Curriculum Assistant</span>
777777
</div>
778778
</div>
779779
</div>
780-
<button class="lms-close-btn" onclick="toggleLmsAiDrawer()" aria-label="Close">×</button>
780+
<div class="copilot-actions"><!-- COPILOT_EXPAND --><button class="lms-close-btn" onclick="toggleLmsAiDrawer()" aria-label="Close">×</button></div>
781781
</div>
782782
783+
<!-- COPILOT_NOTICE -->
783784
<div id="lms-chat-messages" class="lms-chat-messages">
784785
<div class="chat-bubble chat-bubble-assistant">
785786
<div class="chat-bubble-sender">Academic Copilot</div>
@@ -805,7 +806,7 @@ fn render_lms_ai_widget(csrf_token: &str) -> String {
805806
<span style="font-size: 0.72rem; color: #a1a1aa; margin-left: 6px;">Copilot is thinking...</span>
806807
</div>
807808
808-
<div class="lms-cloud-choice"><label><input type="checkbox" name="cloud_ai" value="yes" form="lms-chat-form" autocomplete="off">Send this message to Groq for a cloud AI reply (optional).</label><a href="/privacy">Privacy details</a> · Leave unchecked for a local reply. Do not send personal data.</div>
809+
<div class="lms-cloud-choice copilot-cloud"><label><input type="checkbox" name="cloud_ai" value="yes" form="lms-chat-form" autocomplete="off"><span><strong>Check this box to use cloud AI.</strong> Send my message to Groq (optional).</span></label><span class="copilot-choice-help">Unchecked = local replies. Avoid personal data. <a href="/privacy" target="_blank" rel="noopener noreferrer">Privacy details</a></span></div>
809810
<form id="lms-chat-form" class="lms-ai-form" method="post" action="/api/lms-chat"
810811
hx-post="/api/lms-chat"
811812
hx-target="#lms-chat-messages"
@@ -845,22 +846,7 @@ fn render_lms_ai_widget(csrf_token: &str) -> String {
845846
});
846847
847848
function toggleLmsAiDrawer() {
848-
var drawer = document.getElementById('lms-ai-drawer');
849-
var launcher = document.getElementById('lms-crab-launcher');
850-
if (!drawer) return;
851-
var isOpen = drawer.style.display !== 'none';
852-
if (isOpen) {
853-
drawer.style.display = 'none';
854-
if (launcher) launcher.style.display = 'flex';
855-
document.body.style.overflow = '';
856-
} else {
857-
drawer.style.display = 'flex';
858-
if (launcher) launcher.style.display = 'none';
859-
if (window.innerWidth <= 640) document.body.style.overflow = 'hidden';
860-
var input = document.getElementById('lms-message-input');
861-
if (input) setTimeout(function() { input.focus(); }, 150);
862-
scrollLmsToBottom();
863-
}
849+
window.RullstCopilot.toggle('lms-ai-drawer');
864850
}
865851
866852
function scrollLmsToBottom() {
@@ -926,4 +912,6 @@ fn render_lms_ai_widget(csrf_token: &str) -> String {
926912
});
927913
</script>
928914
"##.replace("__CSRF_TOKEN__", &rullst::html::escape_str(csrf_token))
915+
.replace("<!-- COPILOT_EXPAND -->", blueprint_ai::COPILOT_EXPAND)
916+
.replace("<!-- COPILOT_NOTICE -->", blueprint_ai::COPILOT_NOTICE)
929917
}

blueprints/lms/src/showcase.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ pub async fn css() -> impl IntoResponse {
9696
format!(
9797
"{}{}",
9898
include_str!("../static/showcase.css"),
99-
blueprint_ai::STYLES
99+
blueprint_ai::STYLES.to_owned() + blueprint_ai::COPILOT_STYLES
100100
),
101101
)
102102
}
@@ -106,7 +106,7 @@ pub async fn js() -> impl IntoResponse {
106106
header::CONTENT_TYPE,
107107
"application/javascript; charset=utf-8",
108108
)],
109-
include_str!("../static/showcase.js"),
109+
include_str!("../static/showcase.js").to_owned() + blueprint_ai::COPILOT_SCRIPT,
110110
)
111111
}
112112
pub async fn tailwind() -> impl IntoResponse {

blueprints/portfolio/src/pages/home.rs

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ fn render_ai_widget(csrf_token: &str) -> String {
762762
</div>
763763
</div>
764764
765-
<div id="ai-drawer" class="ai-drawer" style="display: none;" role="dialog" aria-label="AI Career Copilot">
765+
<div id="ai-drawer" data-copilot data-copilot-launcher="ai-crab-launcher" data-copilot-backdrop="" class="ai-drawer" style="display: none;" role="dialog" aria-label="AI Career Copilot">
766766
<div class="ai-drawer-header">
767767
<div class="ai-header-left" style="display: flex; align-items: center; gap: 10px;">
768768
<div class="ai-avatar-badge" style="width: 34px; height: 34px; border-radius: 10px; background: rgba(0, 255, 204, 0.15); border: 1px solid rgba(0, 255, 204, 0.3); display: flex; align-items: center; justify-content: center; font-size: 1.1rem;">⚡</div>
@@ -774,9 +774,10 @@ fn render_ai_widget(csrf_token: &str) -> String {
774774
</div>
775775
</div>
776776
</div>
777-
<button class="ai-close-btn" onclick="toggleAiDrawer()" aria-label="Close">×</button>
777+
<div class="copilot-actions"><!-- COPILOT_EXPAND --><button class="ai-close-btn" onclick="toggleAiDrawer()" aria-label="Close">×</button></div>
778778
</div>
779779
780+
<!-- COPILOT_NOTICE -->
780781
<div id="ai-chat-messages" class="ai-chat-messages">
781782
<div class="chat-bubble chat-bubble-assistant">
782783
<div class="chat-bubble-sender">Career Copilot</div>
@@ -802,7 +803,7 @@ fn render_ai_widget(csrf_token: &str) -> String {
802803
<span style="font-size: 0.72rem; color: #a1a1aa; margin-left: 6px;">Copilot is thinking...</span>
803804
</div>
804805
805-
<div class="portfolio-cloud-choice"><label><input type="checkbox" name="cloud_ai" value="yes" form="ai-chat-form" autocomplete="off"> Use cloud AI: send my message to Groq (optional).</label><span>Uncheck to keep future messages local. <a href="/privacy">Privacy notice</a></span></div>
806+
<div class="portfolio-cloud-choice copilot-cloud"><label><input type="checkbox" name="cloud_ai" value="yes" form="ai-chat-form" autocomplete="off"><span><strong>Check this box to use cloud AI.</strong> Send my message to Groq (optional).</span></label><span class="copilot-choice-help">Unchecked = local replies. Avoid personal data. <a href="/privacy" target="_blank" rel="noopener noreferrer">Privacy details</a></span></div>
806807
<form id="ai-chat-form" class="ai-form"
807808
hx-post="/api/chat"
808809
hx-target="#ai-chat-messages"
@@ -842,22 +843,7 @@ fn render_ai_widget(csrf_token: &str) -> String {
842843
});
843844
844845
function toggleAiDrawer() {
845-
var drawer = document.getElementById('ai-drawer');
846-
var launcher = document.getElementById('ai-crab-launcher');
847-
if (!drawer) return;
848-
var isOpen = drawer.style.display !== 'none';
849-
if (isOpen) {
850-
drawer.style.display = 'none';
851-
if (launcher) launcher.style.display = 'flex';
852-
document.body.style.overflow = '';
853-
} else {
854-
drawer.style.display = 'flex';
855-
if (launcher) launcher.style.display = 'none';
856-
if (window.innerWidth <= 640) document.body.style.overflow = 'hidden';
857-
var input = document.getElementById('ai-message-input');
858-
if (input && window.innerWidth > 640) setTimeout(function() { input.focus(); }, 150);
859-
scrollAiToBottom();
860-
}
846+
window.RullstCopilot.toggle('ai-drawer');
861847
}
862848
863849
function scrollAiToBottom() {
@@ -923,6 +909,8 @@ fn render_ai_widget(csrf_token: &str) -> String {
923909
});
924910
</script>
925911
"##.replace("__CSRF_TOKEN__", &rullst::html::escape_str(csrf_token))
912+
.replace("<!-- COPILOT_EXPAND -->", blueprint_ai::COPILOT_EXPAND)
913+
.replace("<!-- COPILOT_NOTICE -->", blueprint_ai::COPILOT_NOTICE)
926914
}
927915

928916
pub fn render(

blueprints/portfolio/src/showcase.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub async fn css() -> impl IntoResponse {
1111
"{}{}{}",
1212
include_str!("../static/showcase.css"),
1313
include_str!("../static/nexus-responsive.css"),
14-
blueprint_ai::STYLES
14+
blueprint_ai::STYLES.to_owned() + blueprint_ai::COPILOT_STYLES
1515
),
1616
)
1717
}
@@ -21,7 +21,7 @@ pub async fn js() -> impl IntoResponse {
2121
header::CONTENT_TYPE,
2222
"application/javascript; charset=utf-8",
2323
)],
24-
include_str!("../static/showcase.js"),
24+
include_str!("../static/showcase.js").to_owned() + blueprint_ai::COPILOT_SCRIPT,
2525
)
2626
}
2727
pub async fn tailwind() -> impl IntoResponse {

crates/blueprint-ai/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ mod render;
66

77
pub use provider::{AiFailure, chat};
88
pub use render::{STYLES, render_markdown, render_offline_html};
9+
10+
pub const COPILOT_STYLES: &str = include_str!("../static/copilot.css");
11+
pub const COPILOT_SCRIPT: &str = include_str!("../static/copilot.js");
12+
pub const COPILOT_NOTICE: &str = r#"<p class="copilot-notice">AI replies may be incorrect or imprecise. Check the <a href="https://rullst.win/Rullst/book/" target="_blank" rel="noopener noreferrer">official documentation</a>.</p>"#;
13+
pub const COPILOT_EXPAND: &str = r#"<button type="button" class="copilot-expand" data-copilot-expand aria-pressed="false" aria-label="Expand chat to full screen" title="Expand chat to full screen">&#x26F6; Expand</button>"#;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* Shared by the three public crab copilots. Keep controls visible at both sizes. */
2+
[data-copilot] { box-sizing: border-box; }
3+
[data-copilot] .copilot-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
4+
[data-copilot] .copilot-expand { display: inline-flex; align-items: center; justify-content: center; width: auto; min-height: 44px; margin: 0; padding: 0 8px; border: 1px solid #47657d; border-radius: 8px; background: #182f44; color: #e0f2fe; font: 600 12px/1.2 system-ui, sans-serif; cursor: pointer; white-space: nowrap; }
5+
[data-copilot] .copilot-expand:hover { background: #244760; }
6+
[data-copilot] .copilot-expand:focus-visible, [data-copilot] input[type="checkbox"]:focus-visible { outline: 3px solid #7dd3fc; outline-offset: 3px; }
7+
[data-copilot] .copilot-notice { flex-shrink: 0; margin: 0; padding: 8px 12px; background: #29231a; border-bottom: 1px solid #69532d; color: #fde8b6; font: 12px/1.4 system-ui, sans-serif; text-align: left; }
8+
[data-copilot] .copilot-notice a { color: #fde8b6; text-decoration: underline; }
9+
[data-copilot] .copilot-cloud { flex-shrink: 0; padding: 8px 12px; background: #0c1826; border-top: 1px solid #334155; }
10+
[data-copilot] .copilot-cloud label { display: flex; align-items: flex-start; gap: 10px; padding: 9px; margin: 0 0 4px; border: 1px solid #4c7898; border-radius: 8px; background: #142b40; color: #e0f2fe; cursor: pointer; font: 12px/1.4 system-ui, sans-serif; }
11+
[data-copilot] .copilot-cloud label:has(:checked) { border-color: #6ee7b7; background: #123a32; }
12+
[data-copilot] .copilot-cloud input[type="checkbox"] { appearance: auto; width: 22px; height: 22px; flex: 0 0 22px; margin: 0; padding: 0; accent-color: #0ea5e9; cursor: pointer; }
13+
[data-copilot] .copilot-cloud label span { margin: 0; }
14+
[data-copilot] .copilot-choice-help { display: block; color: #afc3d5; font: 11px/1.4 system-ui, sans-serif; }
15+
[data-copilot] :is(.ai-drawer-header,.lms-drawer-header) { gap: 6px; padding: 8px 12px; }
16+
[data-copilot] :is(.ai-drawer-header,.lms-drawer-header) > :first-child { min-width: 0; }
17+
[data-copilot] :is(.ai-close-btn,.lms-close-btn) { min-width: 36px; min-height: 44px; margin: 0; padding: 0; }
18+
[data-copilot] :is(.ai-chat-messages,.lms-chat-messages) { min-height: 60px; }
19+
[data-copilot] :is(.ai-input,.lms-input) { font-size: 16px; min-width: 0; }
20+
[data-copilot] :is(.ai-submit-btn,.lms-submit-btn) { min-height: 44px; }
21+
:is(#showcase-ai-drawer,#ai-drawer,#lms-ai-drawer)[data-copilot].copilot-expanded {
22+
top: var(--copilot-top, 0px) !important; left: var(--copilot-left, 0px) !important; right: auto !important; bottom: auto !important;
23+
width: var(--copilot-width, 100vw) !important; height: var(--copilot-height, 100dvh) !important;
24+
max-width: none !important; max-height: none !important; border-radius: 0 !important; z-index: 11000;
25+
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
26+
}
27+
[data-copilot].copilot-short { overflow-y: auto; }
28+
[data-copilot].copilot-short :is(.ai-prompt-suggestions,.lms-prompt-suggestions,.copilot-choice-help) { display: none; }
29+
[data-copilot].copilot-short .copilot-cloud { padding: 4px 10px; }
30+
[data-copilot].copilot-short .copilot-cloud label { margin: 0; padding: 5px; }
31+
[data-copilot].copilot-short .copilot-notice { padding: 4px 12px; }
32+
[data-copilot].copilot-short :is(.ai-drawer-header,.lms-drawer-header) { padding-top: 0; padding-bottom: 0; }
33+
[data-copilot].copilot-short :is(.ai-chat-messages,.lms-chat-messages) { flex-shrink: 0; }
34+
@media(max-width:760px) {
35+
:is(#showcase-ai-drawer,#ai-drawer,#lms-ai-drawer)[data-copilot]:not(.copilot-expanded) {
36+
bottom: calc(var(--copilot-bottom, 0px) + max(12px, env(safe-area-inset-bottom))) !important;
37+
max-height: calc(var(--copilot-height, 100dvh) - 24px - env(safe-area-inset-bottom)) !important;
38+
}
39+
}
40+
@media(max-width:480px) {
41+
[data-copilot] :is(.ai-drawer-header,.lms-drawer-header) > :first-child > :first-child { display: none !important; }
42+
[data-copilot] :is(.ai-header-sub,.copilot-subtitle) { display: none !important; }
43+
[data-copilot] :is(.ai-drawer-header,.lms-drawer-header) { min-height: 52px; }
44+
}
45+
@media(prefers-reduced-motion:reduce) { [data-copilot] { animation: none !important; } }

0 commit comments

Comments
 (0)