/**
* React/Preact components for WikiShield settings interface
*/
import { h, Component } from 'preact';
/**
* Toggle Switch Component
*/
export class Toggle extends Component {
render() {
const { value, onChange, label, description, id } = this.props;
let currentValue = value;
return (
<div
id={id || ''}
class={`settings-toggle ${value ? 'active' : ''}`}
onClick={e => {
currentValue = !currentValue;
e.target.closest('.settings-toggle').classList.toggle('active', currentValue);
onChange(currentValue);
}}
>
<div class="toggle-switch">
<div class="toggle-slider"></div>
</div>
</div>
);
}
}
export class Radio extends Component {
render() {
const { value, options = [], onChange, name, id } = this.props;
return (
<div
id={id || ''}
class="settings-radio-group"
>
{options.map((option) => (
<div
data-key={option.value}
class={`settings-radio-option ${value === option.value ? 'selected' : ''}`}
onClick={e => {
e.target.closest('.settings-radio-group').querySelectorAll('.settings-radio-option.selected').forEach(el => el.classList.remove('selected'));
e.target.classList.add('selected');
onChange(option.value);
}}
>
{option.label}
</div>
))}
</div>
);
}
}
/**
* Numeric Input Component
*/
export class NumericInput extends Component {
constructor(props) {
super(props);
this.state = {
inputValue: props.value
};
}
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) {
this.setState({ inputValue: this.props.value });
}
}
handleMinus = () => {
const { min, step, onChange } = this.props;
const currentValue = Number(this.state.inputValue);
const newValue = Math.round(Math.max(currentValue - step, min) * 100) / 100;
this.setState({ inputValue: newValue });
onChange(newValue);
}
handlePlus = () => {
const { max, step, onChange } = this.props;
const currentValue = Number(this.state.inputValue);
const newValue = Math.round(Math.min(currentValue + step, max) * 100) / 100;
this.setState({ inputValue: newValue });
onChange(newValue);
}
handleInputChange = () => {
const { min, max, step, onChange } = this.props;
const { inputValue } = this.state;
if (isNaN(Number(inputValue))) {
this.setState({ inputValue: this.props.value });
return;
}
let newValue = Math.round(Math.min(Math.max(Number(inputValue), min), max) * 100) / 100;
newValue = step >= 1 ? Math.round(newValue) : newValue;
this.setState({ inputValue: newValue });
onChange(newValue);
}
handleKeyUp = (e) => {
if (e.key.toLowerCase() === "enter") {
this.handleInputChange();
e.target.blur();
}
}
render() {
const { inputValue } = this.state;
return (
<div class="numeric-input-container">
<span
class="fa fa-minus numeric-input-button"
onClick={this.handleMinus}
></span>
<input
type="name"
class="numeric-input"
value={inputValue}
onInput={(e) => this.setState({ inputValue: e.target.value })}
onBlur={this.handleInputChange}
onKeyUp={this.handleKeyUp}
autoComplete="off"
/>
<span
class="fa fa-plus numeric-input-button"
onClick={this.handlePlus}
></span>
</div>
);
}
}
/**
* Volume Control Component
*/
export class VolumeControl extends Component {
constructor(props) {
super(props);
this.state = {
currentSound: props.currentSound || props.triggerKey
};
}
playSound = () => {
const { playFunction } = this.props;
if (playFunction) playFunction();
}
render() {
const { title, description, value, soundOptions, onVolumeChange, onSoundChange, sounds } = this.props;
const { currentSound } = this.state;
return (
<div class="audio-volume-control">
<div class="volume-control-header">
<div class="volume-control-info">
<div class="volume-control-title">{title}</div>
<div class="volume-control-desc">{description}</div>
</div>
<button
class="volume-control-preview"
onClick={this.playSound}
title="Preview sound"
>
<span class="fa fa-play"></span>
</button>
</div>
<div class="volume-control-main">
<select
class="volume-control-sound-select"
value={currentSound}
onChange={(e) => {
this.setState({ currentSound: e.target.value });
onSoundChange(e.target.value);
}}
>
{soundOptions}
</select>
<div class="volume-control-slider-container">
<span class="fa fa-volume-down"></span>
<input
type="range"
class="volume-control-slider"
min="0"
max="1"
step="0.01"
value={value}
onInput={(e) => onVolumeChange(parseFloat(e.target.value))}
autoComplete="off"
/>
<span class="fa fa-volume-up"></span>
<span class="volume-control-value">{Math.round(value * 100)}%</span>
</div>
</div>
</div>
);
}
}
/**
* Settings Section Component
*/
export class SettingsSection extends Component {
render() {
const { title, description, compact, inline, children, id } = this.props;
return (
<div class={`settings-section ${compact ? 'compact' : ''} ${inline ? 'inline' : ''}`} id={id || ''}>
{title && <div class="settings-section-title">{title}</div>}
{description && <div class="settings-section-desc">{description}</div>}
{children}
</div>
);
}
}
export class SettingsSectionContent extends Component {
render() {
const { title, description, children, id } = this.props;
return (
<div class={`settings-section-content`} id={id || ''}>
{title && <div class="settings-section-title">{title}</div>}
{description && <div class="settings-section-desc">{description}</div>}
{children}
</div>
);
}
}
export class SettingsTogglesSection extends Component {
render() {
const { title, description, children } = this.props;
return (
<div class="settings-toggles-section">
{title && <div class="settings-section-title">{title}</div>}
{description && <div class="settings-section-desc">{description}</div>}
{children}
</div>
);
}
}
/**
* Settings Compact Grid Component
*/
export class SettingsCompactGrid extends Component {
render() {
return (
<div class="settings-compact-grid">
{this.props.children}
</div>
);
}
}
export class DraggableOrderList extends Component {
constructor(props) {
super(props);
this.state = {
items: [],
draggedIndex: null,
placeholderIndex: null
};
this.listRef = null;
}
componentDidMount() {
this.syncItemsFromChildren();
}
componentDidUpdate(prevProps) {
if (prevProps.children !== this.props.children && this.state.draggedIndex === null) {
this.syncItemsFromChildren();
}
}
syncItemsFromChildren = () => {
const { children } = this.props;
if (Array.isArray(children)) {
this.setState({ items: children.map((child, i) => ({ child, key: child.key || i })) });
}
}
handleDragStart = (index, e) => {
this.setState({ draggedIndex: index, placeholderIndex: index });
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', index);
// Add a slight delay to allow the drag image to be captured
requestAnimationFrame(() => {
this.setState(state => ({ ...state }));
});
}
handleDragOver = (index, e) => {
e.preventDefault();
const { draggedIndex, placeholderIndex } = this.state;
if (draggedIndex === null || index === placeholderIndex) return;
// Reorder items in real-time
this.setState(state => {
const newItems = [...state.items];
const draggedItem = newItems[state.draggedIndex];
// Remove from old position
newItems.splice(state.draggedIndex, 1);
// Insert at new position
newItems.splice(index, 0, draggedItem);
return {
items: newItems,
draggedIndex: index,
placeholderIndex: index
};
});
}
handleDragEnd = () => {
const { onReorder } = this.props;
const { items } = this.state;
// Notify parent of the final order
if (onReorder) {
onReorder(items.map(item => item.key));
}
this.setState({ draggedIndex: null, placeholderIndex: null });
}
render() {
const { items, draggedIndex } = this.state;
const isDragging = draggedIndex !== null;
return (
<div class={`draggable-order-list ${isDragging ? 'is-dragging' : ''}`} ref={el => this.listRef = el}>
{items.map((item, index) => {
const isThisDragging = draggedIndex === index;
return (
<div
key={item.key}
class={`draggable-order-item-wrapper ${isThisDragging ? 'dragging' : ''}`}
draggable
onDragStart={(e) => this.handleDragStart(index, e)}
onDragOver={(e) => this.handleDragOver(index, e)}
onDragEnd={this.handleDragEnd}
>
{item.child}
</div>
);
})}
</div>
);
}
}
export class DraggableOrderItem extends Component {
handleToggle = (e) => {
e.stopPropagation();
const { onToggle, enabled } = this.props;
if (onToggle) {
onToggle(!enabled);
}
}
render() {
const { name, enabled = true } = this.props;
return (
<div class={`draggable-order-item ${enabled ? '' : 'disabled'}`}>
<span class="draggable-order-item-name">{name}</span>
<div
class="draggable-order-item-toggle"
onClick={this.handleToggle}
title={enabled ? 'Click to disable' : 'Click to enable'}
/>
</div>
);
}
}
/**
* General Settings Panel Component
*/
export class GeneralSettings extends Component {
render() {
const {
openExternally,
maxEditCount,
maxQueueSize,
minOresScore,
watchlistExpiry,
namespaces,
selectedNamespaces,
onMaxEditCountChange,
onMaxQueueSizeChange,
onMinOresScoreChange,
onNamespaceToggle,
} = this.props;
return (
<div>
<SettingsCompactGrid>
<SettingsSection
compact
id="open-externally"
title="Open Wikishield in new tab"
description="When enabled, WikiShield will open in a new browser tab instead of the current one."
>
<Toggle
value={openExternally}
onChange={(value) => {
localStorage.setItem("WikiShield:OpenExternally", value);
this.props.onOpenExternallyChange(value);
}}
/>
</SettingsSection>
<SettingsSection
compact
id="maximum-edit-count"
title="Maximum edit count"
description="Edits from users with more than this edit count will not be shown"
>
<NumericInput
value={maxEditCount}
min={0}
max={1000000}
step={50}
onChange={onMaxEditCountChange}
/>
</SettingsSection>
<SettingsSection
compact
id="maximum-queue-size"
title="Maximum queue size"
description="The queue will not load additional edits after reaching this size"
>
<NumericInput
value={maxQueueSize}
min={1}
max={500}
step={1}
onChange={onMaxQueueSizeChange}
/>
</SettingsSection>
<SettingsSection
compact
id="minimum-ores-score"
title="Minimum ORES score"
description="The minimum ORES score required to show an edit in the recent changes queue"
>
<NumericInput
value={minOresScore}
min={0}
max={1}
step={0.05}
onChange={onMinOresScoreChange}
/>
</SettingsSection>
<SettingsSection
compact
id="watchlist-expiry"
title="Watchlist expiry for warned users"
description="How long to watch user talk pages after issuing warnings"
>
<select
value={watchlistExpiry}
onChange={(e) => onWatchlistExpiryChange(e.target.value)}
>
<option value="none">None</option>
<option value="1 hour">1 hour</option>
<option value="1 day">1 day</option>
<option value="1 week">1 week</option>
<option value="1 month">1 month</option>
<option value="3 months">3 months</option>
<option value="6 months">6 months</option>
<option value="indefinite">Indefinite</option>
</select>
</SettingsSection>
</SettingsCompactGrid>
<SettingsSection
title="Namespaces to show"
description="Only edits from the selected namespaces will be shown in your queue."
>
<div class="checkbox-container">
{Object.entries(namespaces).map(([key, namespace]) => (
<div class="namespace-item" key={key}>
<label class="checkbox-box">
<input
type="checkbox"
checked={selectedNamespaces.includes(namespace.id)}
onChange={(e) => {
onNamespaceToggle(namespace.id, e.target.checked);
}}
autoComplete="off"
/>
<div class="checkmark"></div>
</label>
<span>{namespace.name}</span>
</div>
))}
</div>
</SettingsSection>
</div>
);
}
}
export class PerformanceSettings extends Component {
render() {
return (
<div>
<SettingsSection
title="Startup Animation"
description="Enable or disable the startup animation when launching WikiShield"
>
<Radio
id="performance-mode"
name="performance-mode"
value={this.props.startup}
options={[
{ value: 'always_off', label: 'Always Off' },
{ value: 'adaptive', label: 'Adaptive' },
{ value: 'always_on', label: 'Always On' }
]}
onChange={this.props.onStartupChange}
/>
</SettingsSection>
</div>
);
}
}
/**
* Audio Settings Panel Component
*/
export class AudioSettings extends Component {
render() {
const {
volumes,
soundMappings,
sounds,
onVolumeChange,
onSoundChange,
playSound
} = this.props;
// Build sound selector options grouped by category
const soundsByCategory = {};
Object.entries(sounds).forEach(([key, sound]) => {
const category = sound.category || 'other';
if (!soundsByCategory[category]) soundsByCategory[category] = [];
soundsByCategory[category].push({ key, sound });
});
const categoryOrder = ['ui', 'alert', 'warning', 'action', 'alert', 'positive', 'negative', 'other'];
const categoryNames = {
ui: 'UI Sounds',
alert: 'Alerts',
warning: 'Warnings',
action: 'Actions',
alert: 'Alerts',
positive: 'Positive',
negative: 'Negative',
other: 'Other'
};
const buildSoundOptions = () => {
const options = [];
options.push(<option value="none">🔇 Disabled</option>);
categoryOrder.forEach(category => {
if (soundsByCategory[category]) {
const categoryItems = soundsByCategory[category].map(({ key, sound }) => (
<option value={key} key={key}>
{sound.icon || '🔊'} {sound.name || key}
</option>
));
options.push(
<optgroup label={categoryNames[category] || category} key={category}>
{categoryItems}
</optgroup>
);
}
});
return options;
};
const soundOptions = buildSoundOptions();
const audioControls = [
{ key: 'click', title: 'Button Click Sound', description: 'Played when clicking buttons' },
{ key: 'alert', title: 'Alert Sound', description: 'Played for important alerts' },
{ key: 'sparkle', title: 'Sparkle Sound', description: 'Played for positive actions' },
{ key: 'error', title: 'Error Sound', description: 'Played when an error occurs' },
{ key: 'alert', title: 'Alert Sound', description: 'Played for new alerts' },
{ key: 'warning', title: 'Warning Sound', description: 'Played when issuing warnings' },
{ key: 'rollback', title: 'Rollback Sound', description: 'Played when rolling back edits' },
{ key: 'queue', title: 'Queue Update Sound', description: 'Played when the queue updates' }
];
return (
<div>
<SettingsSection
title="Audio Controls"
description="Configure volume and sounds for different actions"
>
{audioControls.map(({ key, title, description }) => (
<VolumeControl
key={key}
triggerKey={key}
title={title}
description={description}
value={volumes[key] ?? 0.5}
currentSound={soundMappings[key] || key}
soundOptions={soundOptions}
sounds={sounds}
onVolumeChange={(value) => onVolumeChange(key, value)}
onSoundChange={(soundKey) => onSoundChange(key, soundKey)}
playFunction={() => playSound(key)}
/>
))}
</SettingsSection>
</div>
);
}
}
/**
* Appearance Settings Panel Component
*/
export class QueueSettings extends Component {
constructor(props) {
super(props);
this.state = {
selectedPalette: props.selectedPalette,
queues: [...props.queues]
};
}
handlePaletteChange = (index) => {
this.setState({ selectedPalette: index });
this.props.onPaletteChange(index);
}
handleReorder = (orderedKeys) => {
const { queues } = this.state;
const { onQueueReorder } = this.props;
const queueMap = new Map(queues);
const newQueues = orderedKeys.map(key => [ key, queueMap.get(key) ]);
this.setState({ queues: newQueues });
if (onQueueReorder) {
onQueueReorder(newQueues);
}
}
handleToggle = (id, enabled) => {
const { queues } = this.state;
const { onQueueToggle } = this.props;
const newQueues = queues.map(([ queueId, data ]) =>
queueId === id ? [ queueId, { ...data, enabled } ] : [ queueId, data ]
);
this.setState({ queues: newQueues });
if (onQueueToggle) {
onQueueToggle(id, enabled);
}
}
render() {
const { colorPalettes } = this.props;
const { selectedPalette, queues } = this.state;
return (
<div>
<SettingsSection
title="Queues"
description="Enable or disable different edit queues, and customize their order"
>
<DraggableOrderList onReorder={this.handleReorder}>
{queues.map(([ id, queue ]) => (
<DraggableOrderItem
key={queue.key}
name={queue.name}
enabled={queue.enabled}
onToggle={(enabled) => this.handleToggle(queue.key, enabled)}
/>
))}
</DraggableOrderList>
</SettingsSection>
<SettingsSection
title="Color Palette"
description="Choose how ORES scores are displayed visually"
>
<div class="palette-selector">
{colorPalettes.map((colors, index) => (
<div
key={index}
class={`palette-option ${selectedPalette === index ? 'selected' : ''}`}
onClick={() => this.handlePaletteChange(index)}
>
<div class="palette-name">Palette {index + 1}</div>
<div class="palette-preview">
{colors.map((color, i) => (
<div
key={i}
class="palette-color"
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
))}
</div>
</SettingsSection>
</div>
);
}
}
/**
* Zen Settings Panel Component
*/
export class ZenSettings extends Component {
render() {
const {
enabled,
sound,
music,
notices,
alerts,
badges,
toasts,
} = this.props;
return (
<div>
<SettingsTogglesSection
title="Zen Mode"
description="Customize your distraction-free reviewing experience"
>
<SettingsSection compact inline>
<SettingsSectionContent
title="Enable Zen Mode"
description="Reduce on-screen distractions while reviewing edits"
/>
<Toggle
id="zen-mode-enable"
value={enabled}
onChange={this.props.onEnableChange}
/>
</SettingsSection>
</SettingsTogglesSection>
<SettingsCompactGrid>
<SettingsSection
compact
title="Enable Sounds"
description="Play sounds in Zen mode"
>
<Toggle
value={sound.enabled}
onChange={this.props.onSoundChange}
/>
</SettingsSection>
<SettingsSection
compact
title="Enable Music"
description="Play background music in Zen mode"
>
<Toggle
value={music.enabled}
onChange={this.props.onMusicChange}
/>
</SettingsSection>
<SettingsSection
compact
title="Alerts"
description="Show alerts in Zen mode"
>
<Toggle
value={alerts.enabled}
onChange={this.props.onAlertsChange}
/>
</SettingsSection>
<SettingsSection
compact
title="Notices"
description="Show notices in Zen mode"
>
<Toggle
value={notices.enabled}
onChange={this.props.onNoticesChange}
/>
</SettingsSection>
<SettingsSection
compact
title="Toasts"
description="Show toast messages in Zen mode"
>
<Toggle
value={toasts.enabled}
onChange={this.props.onToastsChange}
/>
</SettingsSection>
<SettingsSection
compact
title="Notification Badges"
description="Show all notification badges in Zen mode."
>
<Toggle
value={badges.enabled}
onChange={this.props.onBadgesChange}
/>
</SettingsSection>
</SettingsCompactGrid>
</div>
);
}
}
/**
* User List Component (for whitelist/highlight users)
*/
export class UserList extends Component {
render() {
const { users, onRemove, showDates } = this.props;
if (users.length === 0) {
return <div class="user-list-empty">No users in this list</div>;
}
return (
<div class="user-list">
{users.map((user) => (
<div class="user-list-item" key={user.name}>
<span class="user-list-name">{user.name}</span>
{showDates && user.date && (
<span class="user-list-date">{new Date(user.date).toLocaleDateString()}</span>
)}
<button
class="user-list-remove"
onClick={() => onRemove(user.name)}
title="Remove user"
>
<span class="fa fa-times"></span>
</button>
</div>
))}
</div>
);
}
}
/**
* Whitelist Panel Component
*/
export class WhitelistSettings extends Component {
constructor(props) {
super(props);
this.state = {
newUsername: ''
};
}
handleAdd = () => {
const { newUsername } = this.state;
if (newUsername.trim()) {
this.props.onAdd(newUsername.trim());
this.setState({ newUsername: '' });
}
}
render() {
const { users, onRemove } = this.props;
const { newUsername } = this.state;
return (
<div>
<SettingsSection
title="Whitelist"
description="Users on the whitelist will not appear in your queue"
>
<div class="user-list-controls">
<input
type="text"
class="user-list-input"
placeholder="Enter username..."
value={newUsername}
onInput={(e) => this.setState({ newUsername: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') this.handleAdd();
}}
autoComplete="off"
/>
<button
class="user-list-add-button"
onClick={this.handleAdd}
>
<span class="fa fa-plus"></span> Add User
</button>
</div>
<UserList
users={users}
onRemove={onRemove}
showDates={true}
/>
</SettingsSection>
</div>
);
}
}
/**
* Highlight Users Panel Component
*/
export class HighlightSettings extends Component {
constructor(props) {
super(props);
this.state = {
newUsername: ''
};
}
handleAdd = () => {
const { newUsername } = this.state;
if (newUsername.trim()) {
this.props.onAdd(newUsername.trim());
this.setState({ newUsername: '' });
}
}
render() {
const { users, onRemove } = this.props;
const { newUsername } = this.state;
return (
<div>
<SettingsSection
title="highlight Users"
description="Edits from highlight users will be shown with a yellow indicator"
>
<div class="user-list-controls">
<input
type="text"
class="user-list-input"
placeholder="Enter username..."
value={newUsername}
onInput={(e) => this.setState({ newUsername: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') this.handleAdd();
}}
autoComplete="off"
/>
<button
class="user-list-add-button"
onClick={this.handleAdd}
>
<span class="fa fa-plus"></span> Add User
</button>
</div>
<UserList
users={users}
onRemove={onRemove}
showDates={true}
/>
</SettingsSection>
</div>
);
}
}
/**
* Statistics Panel Component
*/
export class StatisticsSettings extends Component {
render() {
const { stats } = this.props;
return (
<div>
<SettingsSection
title="Your Statistics"
description="Your WikiShield usage statistics"
>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{stats.editsReviewed || 0}</div>
<div class="stat-label">Edits Reviewed</div>
</div>
<div class="stat-item">
<div class="stat-value">{stats.editsReverted || 0}</div>
<div class="stat-label">Edits Reverted</div>
</div>
<div class="stat-item">
<div class="stat-value">{stats.warningsIssued || 0}</div>
<div class="stat-label">Warnings Issued</div>
</div>
<div class="stat-item">
<div class="stat-value">{stats.usersReported || 0}</div>
<div class="stat-label">Users Reported</div>
</div>
<div class="stat-item">
<div class="stat-value">{stats.pagesProtected || 0}</div>
<div class="stat-label">Pages Protected</div>
</div>
<div class="stat-item">
<div class="stat-value">{stats.thanksGiven || 0}</div>
<div class="stat-label">Thanks Given</div>
</div>
</div>
</SettingsSection>
</div>
);
}
}
/**
* AI Settings Panel Component
*/
export class AISettings extends Component {
constructor(props) {
super(props);
this.state = {
connectionStatus: '',
modelsStatus: '',
availableModels: [],
testingConnection: false,
loadingModels: false
};
}
testConnection = async () => {
const { ollamaServerUrl, onTestConnection } = this.props;
this.setState({ testingConnection: true, connectionStatus: 'Testing...' });
const result = await onTestConnection();
this.setState({
testingConnection: false,
connectionStatus: result ? 'Connected!' : 'Failed to connect'
});
}
refreshModels = async () => {
const { onRefreshModels } = this.props;
this.setState({ loadingModels: true, modelsStatus: 'Loading...' });
const models = await onRefreshModels();
this.setState({
loadingModels: false,
availableModels: models || [],
modelsStatus: models ? `Found ${models.length} models` : 'Failed to load models'
});
}
render() {
const {
enableOllamaAI,
ollamaServerUrl,
selectedModel,
onEnableChange,
onServerUrlChange,
onModelSelect
} = this.props;
const { connectionStatus, modelsStatus, availableModels, testingConnection, loadingModels } = this.state;
return (
<div>
<SettingsSection
id="enable-ollama-ai"
title="Enable Ollama AI Analysis"
description="Use local AI models with complete privacy. Free & fast."
>
<Toggle
value={enableOllamaAI}
onChange={onEnableChange}
/>
</SettingsSection>
<SettingsSection
id="ollama-server-url"
title="Server URL"
>
<div class="ollama-url-controls">
<input
type="text"
id="ollama-url-input"
value={ollamaServerUrl}
onInput={(e) => onServerUrlChange(e.target.value)}
placeholder="http://localhost:11434"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; margin-bottom: 8px;"
autoComplete="off"
/>
<button
id="test-connection-btn"
onClick={this.testConnection}
disabled={testingConnection}
style="padding: 6px 12px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em;"
>
Test Connection
</button>
<span id="connection-status" style="margin-left: 8px; font-size: 0.9em;">
{connectionStatus}
</span>
</div>
</SettingsSection>
<SettingsSection
id="ollama-model-select"
title="Model Selection"
>
<div class="ollama-model-controls">
<button
id="refresh-models-btn"
onClick={this.refreshModels}
disabled={loadingModels}
style="padding: 6px 12px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em;"
>
<span class="fa fa-sync"></span> Refresh Models
</button>
<span id="models-status" style="margin-left: 8px; font-size: 0.9em;">
{modelsStatus}
</span>
<div style="margin-top: 12px;" id="models-container">
{availableModels.length === 0 ? (
<div style="color: #666; font-style: italic; font-size: 0.9em;">
Click "Refresh Models" to load available models
</div>
) : (
<div class="models-list">
{availableModels.map((model) => (
<div
key={model.name}
class={`model-item ${selectedModel === model.name ? 'selected' : ''}`}
onClick={() => onModelSelect(model.name)}
>
<div class="model-name">{model.name}</div>
<div class="model-size">{model.size}</div>
</div>
))}
</div>
)}
</div>
</div>
</SettingsSection>
<SettingsSection>
<div class="settings-section-title" style="color: #dc3545;">CORS Setup Required</div>
<div class="settings-section-desc" style="background: rgba(255, 243, 205, 0.2); padding: 10px; border-radius: 6px; border-left: 4px solid #ffc107; font-size: 0.9em;">
<strong>Set environment variable:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; color: #333;">OLLAMA_ORIGINS</code>
<br /><strong>Value:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; color: #333;">https://en.wikipedia.org,https://*.wikipedia.org</code>
<br /><br />
<details style="cursor: pointer;">
<summary style="font-weight: 600; margin-bottom: 6px;">Windows (Permanent)</summary>
<ol style="margin: 6px 0; padding-left: 20px; font-size: 0.85em;">
<li>System Properties → Environment Variables</li>
<li>New Variable: <code style="color: #333;">OLLAMA_ORIGINS</code></li>
<li>Value: <code style="color: #333;">https://en.wikipedia.org,https://*.wikipedia.org</code></li>
<li>Restart Ollama</li>
</ol>
</details>
<details style="cursor: pointer;">
<summary style="font-weight: 600; margin-bottom: 6px;">Windows (Temporary)</summary>
<pre style="background: #2d2d2d; color: #f8f8f2; padding: 8px; border-radius: 4px; font-size: 0.8em; margin: 6px 0;">{`$env:OLLAMA_ORIGINS="https://en.wikipedia.org,https://*.wikipedia.org"
ollama serve`}</pre>
</details>
<details style="cursor: pointer;">
<summary style="font-weight: 600; margin-bottom: 6px;">macOS/Linux</summary>
Add to <code>~/.bashrc</code> or <code>~/.zshrc</code>:
<pre style="background: #2d2d2d; color: #f8f8f2; padding: 8px; border-radius: 4px; font-size: 0.8em; margin: 6px 0;">{`export OLLAMA_ORIGINS="https://en.wikipedia.org,https://*.wikipedia.org"`}</pre>
Then: <code>source ~/.bashrc && ollama serve</code>
</details>
</div>
</SettingsSection>
<SettingsSection title="Quick Info">
<div class="settings-section-desc" style="font-size: 0.9em;">
<strong>Get Ollama:</strong> <a href="https://ollama.com" target="_blank" style="color: #667eea; font-weight: bold;">ollama.com</a>
<br /><strong>Popular models:</strong> llama3.2, mistral, gemma2, qwen2.5
<br /><strong>Detects:</strong> Vandalism, spam, POV, attacks, copyright issues, policy violations
</div>
</SettingsSection>
</div>
);
}
}
export class AutoReportingSettings extends Component {
render() {
const {
enableAutoReporting,
autoReportReasons,
selectedAutoReportReasons,
} = this.props;
return (
<div>
<SettingsSection
id="enable-auto-reporting"
title="Enable Auto-Reporting"
description="Automatically report edits that receive certain warnings"
>
<Toggle
value={enableAutoReporting}
onChange={this.props.onEnableChange}
/>
</SettingsSection>
<SettingsSection
title="Auto-Reportable Warnings"
description="Select which warnings should trigger automatic reporting"
>
<div class="checkbox-container">
{autoReportReasons.map((warning) => (
<div class="auto-reportable-warning-item" key={warning}>
<label class="checkbox-box">
<input
type="checkbox"
checked={selectedAutoReportReasons.has(warning)}
onChange={(e) => {
this.props.onWarningToggle(warning, e.target.checked);
}}
autoComplete="off"
/>
<div class="checkmark"></div>
</label>
<span>{warning}</span>
</div>
))}
</div>
</SettingsSection>
</div>
);
}
}
/**
* Import/Export Settings Panel Component
*/
export class SaveSettings extends Component {
constructor(props) {
super(props);
this.state = {
showImportInput: false,
importValue: '',
statusMessage: null
};
}
handleExport = () => {
try {
const result = this.props.onExport();
this.setState({
statusMessage: {
type: 'success',
title: 'Settings exported successfully!',
message: 'The base64 string has been copied to your clipboard.'
}
});
setTimeout(() => this.setState({ statusMessage: null }), 5000);
} catch (error) {
this.setState({
statusMessage: {
type: 'error',
title: 'Export failed!',
message: error.message
}
});
}
}
handleImportToggle = () => {
if (!this.state.showImportInput) {
this.setState({ showImportInput: true, statusMessage: null });
} else {
this.handleImportApply();
}
}
handleImportApply = () => {
const { importValue } = this.state;
if (!importValue.trim()) {
this.setState({
statusMessage: {
type: 'error',
title: 'No input!',
message: 'Please paste a base64 settings string.'
}
});
return;
}
try {
const result = this.props.onImport(importValue);
let warningsHtml = '';
if (result.warnings && result.warnings.length > 0) {
warningsHtml = '<br/><br/><strong>Warnings:</strong><ul>';
result.warnings.forEach(warning => {
warningsHtml += `<li>${warning}</li>`;
});
warningsHtml += '</ul>';
}
this.setState({
statusMessage: {
type: 'success',
title: 'Settings imported successfully!',
message: 'Please reload the page for all changes to take effect.',
extra: warningsHtml
},
showImportInput: false,
importValue: ''
});
} catch (error) {
this.setState({
statusMessage: {
type: 'error',
title: 'Import failed!',
message: error.message
}
});
}
}
handleReset = async () => {
if (await this.props.onReset()) {
this.setState({
statusMessage: {
type: 'success',
title: 'Settings reset successfully!',
message: 'All settings have been restored to default values.'
}
});
}
}
render() {
const {
showImportInput, importValue, statusMessage,
enableCloudStorage,
onCloudStorageToggle
} = this.state;
return (
<div>
<SettingsSection
id="enable-cloud-storage"
title="Enable Cloud Storage"
description="Store your settings in the cloud for access across multiple browsers and devices"
>
<Toggle
value={enableCloudStorage}
onChange={onCloudStorageToggle}
/>
</SettingsSection>
<SettingsSection
title="Import / Export / Reset Settings"
description="Import, export, or reset your WikiShield settings. Settings are encoded as a base64 string for easy sharing."
>
<div class="buttons">
<button
id="export-settings-btn"
onClick={this.handleExport}
>
<span class="fa fa-download"></span> Export Settings
</button>
<button
id="import-settings-btn"
onClick={this.handleImportToggle}
style={`${showImportInput ? '--background: 40, 167, 69, 1;' : ''}`}
>
<span class={`fa ${showImportInput ? 'fa-check' : 'fa-upload'}`}></span>
{showImportInput ? ' Apply Import' : ' Import Settings'}
</button>
<button
id="reset-settings-btn"
onClick={this.handleReset}
style="--background: 211, 51, 51;"
>
<span class="fa fa-undo"></span> Reset to Default
</button>
</div>
{statusMessage && (
<div
id="import-export-status"
class={statusMessage.type === 'success' ? 'status-success' : 'status-error'}
>
<div>
<span class={`fa ${statusMessage.type === 'success' ? 'fa-check-circle' : 'fa-times-circle'}`}></span>
<div>
<strong>{statusMessage.title}</strong>
<div>{statusMessage.message}</div>
{statusMessage.extra && (
<div dangerouslySetInnerHTML={{ __html: statusMessage.extra }} />
)}
</div>
</div>
</div>
)}
{showImportInput && (
<textarea
id="import-settings-input"
placeholder="Paste base64 settings string here..."
value={importValue}
onInput={(e) => this.setState({ importValue: e.target.value })}
resize="vertical"
/>
)}
</SettingsSection>
</div>
);
}
}
/**
* About Panel Component
*/
export class AboutSettings extends Component {
render() {
const { version, changelog } = this.props;
return (
<div>
<SettingsSection title="About WikiShield">
<div class="about-content">
<div class="about-version">
<span class="fa fa-shield-alt"></span>
<span>WikiShield v{version}</span>
</div>
<div class="about-description">
<p>WikiShield is a powerful tool for patrolling Wikipedia edits in real-time.</p>
<p>Developed with ❤️ for the Wikipedia community.</p>
</div>
<div class="about-links">
<a href="https://en.wikipedia.org/wiki/Wikipedia:WikiShield" target="_blank">
<span class="fa fa-book"></span> Documentation
</a>
<a href="https://github.com/LuniZunie/WikiShield" target="_blank">
<span class="fa fa-code-branch"></span> Source Code
</a>
<a href="https://en.wikipedia.org/wiki/Wikipedia_talk:WikiShield" target="_blank">
<span class="fa fa-comments"></span> Feedback
</a>
</div>
</div>
</SettingsSection>
{changelog && (
<SettingsSection>
<div class="changelog-content" dangerouslySetInnerHTML={{ __html: changelog }} />
</SettingsSection>
)}
</div>
);
}
}