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>
194 lines
4.8 KiB
Plaintext
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>
|