groupchat/client/src/components/invite-modal.riot
Jason Tudisco 01258fa958 feat: complete GroupChat app with AI tool calling, search, fetch, and UI
Full-stack real-time group chat with Rust/Axum backend and Riot.js frontend.

Features:
- Auth (register/login/JWT), rooms, invites, WebSocket messaging
- AI responses via OpenRouter with tool calling (Brave Search + web fetch)
- Real-time tool usage indicators (searching/reading page)
- Collapsible tool results in message bubbles
- AI stats bar (model, tokens, speed, response time) persisted to DB
- Room soft-delete, /clear command, dynamic model fetching
- Markdown rendering with code highlighting and copy buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:50:52 -06:00

194 lines
4.8 KiB
Plaintext

<invite-modal>
<div class="modal-overlay" onclick={handleOverlayClick}>
<div class="modal" onclick={e => e.stopPropagation()}>
<div class="modal-header">
<h3>Invite to Room</h3>
<button class="btn btn-ghost btn-icon" onclick={props.cbClose}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<form if={!state.inviteUrl} onsubmit={handleSubmit}>
<div class="form-group">
<label for="invite-email">Email address</label>
<input
type="email"
id="invite-email"
placeholder="friend@example.com"
value={state.email}
oninput={e => update({ email: e.target.value })}
required
/>
</div>
<p if={state.error} class="error-text">{state.error}</p>
<div class="modal-actions">
<button type="button" class="btn btn-ghost" onclick={props.cbClose}>Cancel</button>
<button type="submit" class="btn btn-primary" disabled={state.loading}>
{state.loading ? 'Sending...' : 'Send Invite'}
</button>
</div>
</form>
<div if={state.inviteUrl} class="invite-success">
<p>Invite link generated!</p>
<div class="invite-link-box">
<code>{state.inviteUrl}</code>
<button class="btn btn-ghost btn-sm" onclick={copyLink}>Copy</button>
</div>
<p class="help-text">Share this link with your friend</p>
<div class="modal-actions">
<button class="btn btn-primary" onclick={props.cbClose}>Done</button>
</div>
</div>
</div>
</div>
<style>
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
width: 100%;
max-width: 440px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-lg);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-lg);
}
.modal-header h3 {
font-size: var(--text-lg);
font-weight: 600;
}
.btn-icon {
width: 36px;
height: 36px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
}
.form-group {
margin-bottom: var(--space-md);
}
.form-group label {
display: block;
margin-bottom: var(--space-xs);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.error-text {
color: var(--error);
font-size: var(--text-sm);
margin-bottom: var(--space-sm);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
margin-top: var(--space-lg);
}
.invite-success {
text-align: center;
}
.invite-success > p:first-child {
font-weight: 500;
margin-bottom: var(--space-md);
}
.invite-link-box {
display: flex;
align-items: center;
gap: var(--space-sm);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--space-sm) var(--space-md);
margin-bottom: var(--space-sm);
}
.invite-link-box code {
flex: 1;
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--accent);
word-break: break-all;
}
.btn-sm {
padding: var(--space-xs) var(--space-sm);
font-size: var(--text-sm);
}
.help-text {
font-size: var(--text-xs);
color: var(--text-muted);
}
</style>
<script>
import { api } from '../services/api.js'
export default {
state: {
email: '',
error: null,
loading: false,
inviteUrl: null,
},
handleOverlayClick() {
this.props.cbClose()
},
async handleSubmit(e) {
e.preventDefault()
this.update({ loading: true, error: null })
try {
const result = await api.createInvite({
room_id: this.props.roomId,
email: this.state.email,
})
this.update({
inviteUrl: `${window.location.origin}${result.invite_url}`,
loading: false,
})
} catch (err) {
this.update({ error: err.message, loading: false })
}
},
copyLink() {
navigator.clipboard.writeText(this.state.inviteUrl)
},
}
</script>
</invite-modal>