diff --git a/src/apps/canvas/app.ts b/src/apps/canvas/app.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b6c450a26893adb7faf2f05bc704954e54f3bed4
--- /dev/null
+++ b/src/apps/canvas/app.ts
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Viewer from 'mol-view/viewer';
+import { getCifFromUrl, getModelsFromMmcif } from './util';
+import { StructureView } from './view';
+import { BehaviorSubject } from 'rxjs';
+
+export class App {
+    viewer: Viewer
+    container: HTMLDivElement | null = null;
+    canvas: HTMLCanvasElement | null = null;
+    structureView: StructureView | null = null;
+
+    pdbIdLoaded: BehaviorSubject<StructureView | null> = new BehaviorSubject<StructureView | null>(null)
+
+    initViewer(_canvas: HTMLCanvasElement, _container: HTMLDivElement) {
+        this.canvas = _canvas
+        this.container = _container
+
+        try {
+            this.viewer = Viewer.create(this.canvas, this.container)
+            this.viewer.animate()
+            return true
+        } catch (e) {
+            console.error(e)
+            return false
+        }
+    }
+
+    async loadPdbId(id: string) {
+        if (this.structureView) this.structureView.destroy()
+        const cif = await getCifFromUrl(`https://files.rcsb.org/download/${id}.cif`)
+        const models = await getModelsFromMmcif(cif)
+        this.structureView = await StructureView(this.viewer, models, { assembly: '1' })
+        this.pdbIdLoaded.next(this.structureView)
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/assembly-symmetry.ts b/src/apps/canvas/assembly-symmetry.ts
index ecf11d1ae6562642350f95eeb8c7b755109295fc..e5b916802337473d2a5c5b7860fb1056e2b5b37c 100644
--- a/src/apps/canvas/assembly-symmetry.ts
+++ b/src/apps/canvas/assembly-symmetry.ts
@@ -4,17 +4,14 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { AssemblySymmetry } from "mol-model-props/rcsb/symmetry";
-import { Table } from "mol-data/db";
-import { Color } from "mol-util/color";
-import { MeshBuilder } from "mol-geo/mesh/mesh-builder";
-import { Tensor } from "mol-math/linear-algebra";
-import { addSphere } from "mol-geo/mesh/builder/sphere";
-import { addCylinder } from "mol-geo/mesh/builder/cylinder";
-import { Shape } from "mol-model/shape";
-import { DefaultView } from "./view";
-import { Model } from "mol-model/structure";
-import { ShapeRepresentation, ShapeProps } from "mol-geo/representation/shape";
+import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { Table } from 'mol-data/db';
+import { Color } from 'mol-util/color';
+import { MeshBuilder } from 'mol-geo/mesh/mesh-builder';
+import { Tensor } from 'mol-math/linear-algebra';
+import { addSphere } from 'mol-geo/mesh/builder/sphere';
+import { addCylinder } from 'mol-geo/mesh/builder/cylinder';
+import { Shape } from 'mol-model/shape';
 
 export function getAxesShape(featureId: number, assemblySymmetry: AssemblySymmetry) {
     const f = assemblySymmetry.db.rcsb_assembly_symmetry_feature
@@ -59,39 +56,4 @@ export function getClusterColorTheme(featureId: number, assemblySymmetry: Assemb
     for (let i = 0, il = clusters._rowCount; i < il; ++i) {
         console.log(clusters.members.value(i), clusters.avg_rmsd.value(i), feature.stoichiometry_value, feature.stoichiometry_description)
     }
-}
-
-export interface SymmetryView extends DefaultView {
-    readonly axes: ShapeRepresentation<ShapeProps> // TODO
-}
-
-export async function SymmetryView(model: Model, assembly: string): Promise<SymmetryView> {
-    const view = await DefaultView(model, assembly)
-    const axesRepr = ShapeRepresentation()
-
-    await AssemblySymmetry.attachFromCifOrAPI(model)
-    const assemblySymmetry = AssemblySymmetry.get(model)
-    console.log(assemblySymmetry)
-    if (assemblySymmetry) {
-        const features = assemblySymmetry.getFeatures(assembly)
-        if (features._rowCount) {
-            const axesShape = getAxesShape(features.id.value(1), assemblySymmetry)
-            console.log(axesShape)
-            if (axesShape) {
-                
-                await axesRepr.create(axesShape, {
-                    colorTheme: { name: 'shape-group' },
-                    // colorTheme: { name: 'uniform', value: Color(0xFFCC22) },
-                    useFog: false // TODO fog not working properly
-                }).run()
-            }
-
-            getClusterColorTheme(features.id.value(0), assemblySymmetry)
-            getClusterColorTheme(features.id.value(1), assemblySymmetry)
-        }
-    }
-
-    return Object.assign({}, view, {
-        axes: axesRepr
-    })
 }
\ No newline at end of file
diff --git a/src/apps/canvas/component/app.tsx b/src/apps/canvas/component/app.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..73fcbdcec1f0b900ba1d288ad7ea076b6f9772a2
--- /dev/null
+++ b/src/apps/canvas/component/app.tsx
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureView } from '../view';
+import { App } from '../app';
+import { Viewport } from './viewport';
+import { StructureComponent } from './structure';
+
+// export function FileInput (props: {
+//     accept: string
+//     onChange: (v: FileList | null) => void,
+// }) {
+//     return <input
+//         accept={props.accept || '*.*'}
+//         type='file'
+//         onChange={e => props.onChange.call(null, e.target.files)}
+//     />
+// }
+
+export interface AppProps {
+    app: App
+}
+
+export interface AppState {
+    structureView: StructureView | null
+}
+
+export class AppComponent extends React.Component<AppProps, AppState> {
+    state = {
+        structureView: this.props.app.structureView,
+    }
+
+    componentDidMount() {
+        this.props.app.pdbIdLoaded.subscribe(() => this.setState({
+            structureView: this.props.app.structureView
+        }))
+    }
+
+    render() {
+        const { structureView } = this.state
+
+        return <div style={{width: '100%', height: '100%'}}>
+            <div style={{float: 'left', width: '70%', height: '100%'}}>
+                <Viewport app={this.props.app} />
+            </div>
+
+            <div style={{float: 'right', width: '25%', height: '100%'}}>
+                {structureView ? <StructureComponent structureView={structureView} /> : ''}
+            </div>
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/component/structure.tsx b/src/apps/canvas/component/structure.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..124ccc6847816f24dcac126185881bbd504a93a9
--- /dev/null
+++ b/src/apps/canvas/component/structure.tsx
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureView } from '../view';
+
+// export function FileInput (props: {
+//     accept: string
+//     onChange: (v: FileList | null) => void,
+// }) {
+//     return <input
+//         accept={props.accept || '*.*'}
+//         type='file'
+//         onChange={e => props.onChange.call(null, e.target.files)}
+//     />
+// }
+
+export interface StructureComponentProps {
+    structureView: StructureView
+}
+
+export interface StructureComponentState {
+    label: string
+    assemblyId: string
+    assemblyIds: { id: string, label: string }[]
+    symmetryFeatureId: number
+    symmetryFeatureIds: { id: number, label: string }[]
+}
+
+export class StructureComponent extends React.Component<StructureComponentProps, StructureComponentState> {
+    state = {
+        label: this.props.structureView.label,
+        assemblyId: this.props.structureView.assemblyId,
+        assemblyIds: this.props.structureView.getAssemblyIds(),
+        symmetryFeatureId: this.props.structureView.symmetryFeatureId,
+        symmetryFeatureIds: this.props.structureView.getSymmetryFeatureIds()
+    }
+
+    componentWillMount() {
+        const sv = this.props.structureView
+
+        this.setState({
+            ...this.state,
+            label: sv.label,
+            assemblyId: sv.assemblyId,
+            assemblyIds: sv.getAssemblyIds(),
+            symmetryFeatureId: sv.symmetryFeatureId,
+            symmetryFeatureIds: sv.getSymmetryFeatureIds()
+        })
+    }
+
+    async update(state: Partial<StructureComponentState>) {
+        const sv = this.props.structureView
+
+        if (state.assemblyId !== undefined) await sv.setAssembly(state.assemblyId)
+        if (state.symmetryFeatureId !== undefined) await sv.setSymmetryFeature(state.symmetryFeatureId)
+
+        const newState = {
+            ...this.state,
+            label: sv.label,
+            assemblyId: sv.assemblyId,
+            assemblyIds: sv.getAssemblyIds(),
+            symmetryFeatureId: sv.symmetryFeatureId,
+            symmetryFeatureIds: sv.getSymmetryFeatureIds()
+        }
+        this.setState(newState)
+    }
+
+    render() {
+        const { label, assemblyIds, symmetryFeatureIds } = this.state
+
+        const assemblyIdOptions = assemblyIds.map(a => {
+            return <option key={a.id} value={a.id}>{a.label}</option>
+        })
+        const symmetryFeatureIdOptions = symmetryFeatureIds.map(f => {
+            return <option key={f.id} value={f.id}>{f.label}</option>
+        })
+
+        return <div>
+            <div>
+                <span>{label}</span>
+            </div>
+            <div>
+                <div>
+                    <span>Assembly</span>
+                    <select
+                        value={this.state.assemblyId}
+                        onChange={(e) => {
+                            this.update({ assemblyId: e.target.value })
+                        }}
+                    >
+                        {assemblyIdOptions}
+                    </select>
+                </div>
+                <div>
+                    <span>Symmetry Feature</span>
+                    <select
+                        value={this.state.symmetryFeatureId}
+                        onChange={(e) => {
+                            this.update({ symmetryFeatureId: parseInt(e.target.value) })
+                        }}
+                    >
+                        {symmetryFeatureIdOptions}
+                    </select>
+                </div>
+            </div>
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/component/viewport.tsx b/src/apps/canvas/component/viewport.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f391125b28d0d2dbe1d4302dc650e27f57b656c5
--- /dev/null
+++ b/src/apps/canvas/component/viewport.tsx
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { App } from '../app';
+import { MarkerAction } from 'mol-geo/util/marker-data';
+import { EveryLoci } from 'mol-model/loci';
+import { labelFirst } from 'mol-view/label';
+
+interface ViewportProps {
+    app: App
+}
+
+interface ViewportState {
+    noWebGl: boolean,
+    info: string
+}
+
+export class Viewport extends React.Component<ViewportProps, ViewportState> {
+    private container: HTMLDivElement | null = null;
+    private canvas: HTMLCanvasElement | null = null;
+
+    state: ViewportState = {
+        noWebGl: false,
+        info: ''
+    };
+
+    handleResize() {
+        this.props.app.viewer.handleResize()
+    }
+
+    componentDidMount() {
+        if (!this.canvas || !this.container || !this.props.app.initViewer(this.canvas, this.container)) {
+            this.setState({ noWebGl: true });
+        }
+        this.handleResize()
+
+        const viewer = this.props.app.viewer
+
+        viewer.input.resize.subscribe(() => this.handleResize())
+
+        viewer.input.move.subscribe(({x, y, inside, buttons}) => {
+            if (!inside || buttons) return
+            const p = viewer.identify(x, y)
+            const loci = viewer.getLoci(p)
+
+            viewer.mark(EveryLoci, MarkerAction.RemoveHighlight)
+            viewer.mark(loci, MarkerAction.Highlight)
+
+            const label = labelFirst(loci)
+            const info = `${label}`
+            this.setState({ info })
+        })
+    }
+
+    componentWillUnmount() {
+        if (super.componentWillUnmount) super.componentWillUnmount();
+        // TODO viewer cleanup
+    }
+
+    renderMissing() {
+        return <div>
+            <div>
+                <p><b>WebGL does not seem to be available.</b></p>
+                <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps.</p>
+                <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p>
+            </div>
+        </div>
+    }
+
+    render() {
+        if (this.state.noWebGl) return this.renderMissing();
+
+        return <div style={{ backgroundColor: 'rgb(0, 0, 0)', width: '100%', height: '100%'}}>
+            <div ref={elm => this.container = elm} style={{width: '100%', height: '100%'}}>
+                <canvas ref={elm => this.canvas = elm}></canvas>
+            </div>
+            <div
+                style={{
+                    position: 'absolute',
+                    top: 10,
+                    left: 10,
+                    padding: 10,
+                    color: 'lightgrey',
+                    background: 'rgba(0, 0, 0, 0.2)'
+                }}
+            >
+                {this.state.info}
+            </div>
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/index.html b/src/apps/canvas/index.html
index 7b2d2512a1552862b42bd98ebe9be164f15812dc..aa8b418e98a6b136e98934a53547f0f03cde5d3b 100644
--- a/src/apps/canvas/index.html
+++ b/src/apps/canvas/index.html
@@ -4,12 +4,20 @@
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
         <title>Mol* Canvas</title>
+        <style>
+                * {
+                    margin: 0;
+                    padding: 0;
+                }
+                html, body {
+                    width: 100%;
+                    height: 100%;
+                    overflow: hidden;
+                }
+              </style>
     </head>
     <body>
-        <div id="container" style="width:1024px; height: 768px;">
-            <canvas id="canvas"></canvas>
-        </div>
-        <span id="info"></span>
+        <div id="app" style="width: 100%; height: 100%"></div>
         <script type="text/javascript" src="./index.js"></script>
     </body>
 </html>
\ No newline at end of file
diff --git a/src/apps/canvas/index.ts b/src/apps/canvas/index.ts
index 41b176944d4f0b3ce9fb76eaab86fb89873db1cb..49d4094c098e08a94eafdab889d7eaca7fdc5453 100644
--- a/src/apps/canvas/index.ts
+++ b/src/apps/canvas/index.ts
@@ -4,57 +4,18 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import './index.html'
-
-import Viewer from 'mol-view/viewer';
-import { EveryLoci } from 'mol-model/loci';
-import { MarkerAction } from 'mol-geo/util/marker-data';
-import { labelFirst } from 'mol-view/label';
-import { getCifFromUrl, getModelFromMmcif } from './util';
-import { SymmetryView } from './assembly-symmetry';
-
-const container = document.getElementById('container')
-if (!container) throw new Error('Can not find element with id "container".')
-
-const canvas = document.getElementById('canvas') as HTMLCanvasElement
-if (!canvas) throw new Error('Can not find element with id "canvas".')
-
-const info = document.getElementById('info')
-if (!info) throw new Error('Can not find element with id "info".')
+import * as React from 'react'
+import * as ReactDOM from 'react-dom'
 
-const viewer = Viewer.create(canvas, container)
-viewer.animate()
-
-viewer.input.resize.subscribe(() => {
-    // do whatever appropriate
-})
-
-viewer.input.move.subscribe(({x, y, inside, buttons}) => {
-    if (!inside || buttons) return
-    const p = viewer.identify(x, y)
-    const loci = viewer.getLoci(p)
-
-    viewer.mark(EveryLoci, MarkerAction.RemoveHighlight)
-    viewer.mark(loci, MarkerAction.Highlight)
-
-    const label = labelFirst(loci)
-    info.innerText = `${label}`
-})
+import './index.html'
 
-async function init() {
-    const assembly = '1'
+import { App } from './app';
+import { AppComponent } from './component/app';
 
-    const cif = await getCifFromUrl('https://files.rcsb.org/download/4hhb.cif')
-    const model = await getModelFromMmcif(cif)
-    
-    const view = await SymmetryView(model, assembly)
-    viewer.center(view.structure.boundary.sphere.center)
-    viewer.add(view.cartoon)
-    viewer.add(view.ballAndStick)
-    viewer.add(view.axes)
+const elm = document.getElementById('app') as HTMLElement
+if (!elm) throw new Error('Can not find element with id "app".')
 
-    // ensure the added representations get rendered, i.e. without mouse input
-    viewer.requestDraw()
-}
+const app = new App()
+ReactDOM.render(React.createElement(AppComponent, { app }), elm);
 
-init()
\ No newline at end of file
+app.loadPdbId('2ONK')
\ No newline at end of file
diff --git a/src/apps/canvas/util.ts b/src/apps/canvas/util.ts
index 6057b09d7a185e9c8110d1821a1502adae303abe..f63797e1016804f9209bd39a2411750f314063a8 100644
--- a/src/apps/canvas/util.ts
+++ b/src/apps/canvas/util.ts
@@ -5,7 +5,7 @@
  */
 
 import CIF, { CifBlock } from 'mol-io/reader/cif'
-import { readUrlAs } from "mol-util/read";
+import { readUrlAs } from 'mol-util/read';
 import { Model, Format, StructureSymmetry, Structure } from 'mol-model/structure';
 // import { parse as parseObj } from 'mol-io/reader/obj/parser'
 
@@ -25,16 +25,15 @@ export async function getCifFromUrl(url: string) {
     return parsed.result.blocks[0]
 }
 
-export async function getModelFromMmcif(cif: CifBlock) {
-    const models = await Model.create(Format.mmCIF(cif)).run()
-    return models[0]
+export async function getModelsFromMmcif(cif: CifBlock) {
+    return await Model.create(Format.mmCIF(cif)).run()
 }
 
-export async function getStructureFromModel(model: Model, assembly = '0') {
+export async function getStructureFromModel(model: Model, assembly: string) {
     const assemblies = model.symmetry.assemblies
-    if (assemblies.length) {
-        return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run()
-    } else {
+    if (assembly === '0') {
         return Structure.ofModel(model)
+    } else if (assemblies.find(a => a.id === assembly)) {
+        return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run()
     }
 }
\ No newline at end of file
diff --git a/src/apps/canvas/view.ts b/src/apps/canvas/view.ts
index c0dba012644e40f303f178a4b958a04cfa356191..ca30b1692b1474d4219d35695b078361c0bf4aee 100644
--- a/src/apps/canvas/view.ts
+++ b/src/apps/canvas/view.ts
@@ -4,54 +4,213 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model, Structure } from "mol-model/structure";
+import { Model, Structure } from 'mol-model/structure';
 import { CartoonRepresentation } from 'mol-geo/representation/structure/representation/cartoon';
-import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick';
-import { getStructureFromModel } from "./util";
+// import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { getStructureFromModel } from './util';
+import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { ShapeRepresentation, ShapeProps } from 'mol-geo/representation/shape';
+import { getAxesShape } from './assembly-symmetry';
+import Viewer from 'mol-view/viewer';
+
+export interface StructureView {
+    readonly label: string
+    readonly models: ReadonlyArray<Model>
+    readonly structure: Structure | undefined
+    readonly assemblySymmetry: AssemblySymmetry | undefined
 
-export interface DefaultView {
-    readonly model: Model
-    readonly structure: Structure
     readonly cartoon: CartoonRepresentation
-    readonly ballAndStick: BallAndStickRepresentation
-    
-    setAssembly(assembly: string): void
+    // readonly ballAndStick: BallAndStickRepresentation
+    readonly axes: ShapeRepresentation<ShapeProps>
+
+    readonly modelId: number
+    readonly assemblyId: string
+    readonly symmetryFeatureId: number
+
+    setAssembly(assembly: string): Promise<void>
+    getAssemblyIds(): { id: string, label: string }[]
+    setSymmetryFeature(symmetryFeature: number): Promise<void>
+    getSymmetryFeatureIds(): { id: number, label: string }[]
+
+    destroy: () => void
 }
 
-export async function DefaultView(model: Model, assembly: string): Promise<DefaultView> {
+interface StructureViewProps {
+    assembly?: string
+    symmetryFeature?: number
+}
+
+export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>, props: StructureViewProps): Promise<StructureView> {
     const cartoon = CartoonRepresentation()
-    const ballAndStick = BallAndStickRepresentation()
-    
-    let structure: Structure
+    // const ballAndStick = BallAndStickRepresentation()
+    const axes = ShapeRepresentation()
+
+    let label: string
+    let model: Model | undefined
+    let assemblySymmetry: AssemblySymmetry | undefined
+    let structure: Structure | undefined
+
+    let modelId: number
+    let assemblyId: string
+    let symmetryFeatureId: number
 
-    async function setAssembly(assembly: string) {
-        structure = await getStructureFromModel(model, assembly)
-        await createRepr()
+    async function setModel(newModelId: number, newAssemblyId?: string, newSymmetryFeatureId?: number) {
+        modelId = newModelId
+
+        model = models[modelId]
+        await AssemblySymmetry.attachFromCifOrAPI(model)
+        assemblySymmetry = AssemblySymmetry.get(model)
+
+        await setAssembly(newAssemblyId, newSymmetryFeatureId)
+    }
+
+    async function setAssembly(newAssemblyId?: string, newSymmetryFeatureId?: number) {
+        if (newAssemblyId !== undefined) {
+            assemblyId = newAssemblyId
+        } else if (model && model.symmetry.assemblies.length) {
+            assemblyId = model.symmetry.assemblies[0].id
+        } else if (model) {
+            assemblyId = '0'
+        } else {
+            assemblyId = '-1'
+        }
+        await getStructure()
+        await createStructureRepr()
+        await setSymmetryFeature(newSymmetryFeatureId)
     }
 
-    async function createRepr() {
-        await cartoon.create(structure, {
-            colorTheme: { name: 'chain-id' },
-            sizeTheme: { name: 'uniform', value: 0.2 },
-            useFog: false // TODO fog not working properly
-        }).run()
-
-        await ballAndStick.create(structure, {
-            colorTheme: { name: 'element-symbol' },
-            sizeTheme: { name: 'uniform', value: 0.1 },
-            useFog: false // TODO fog not working properly
-        }).run()
+    function getAssemblyIds() {
+        const assemblyIds: { id: string, label: string }[] = [
+            { id: '0', label: '0: model' }
+        ]
+        if (model) model.symmetry.assemblies.forEach(a => {
+            assemblyIds.push({ id: a.id, label: `${a.id}: ${a.details}` })
+        })
+        return assemblyIds
     }
 
-    await setAssembly(assembly)
+    async function setSymmetryFeature(newSymmetryFeatureId?: number) {
+        if (newSymmetryFeatureId !== undefined) {
+            symmetryFeatureId = newSymmetryFeatureId
+        } else if (assemblySymmetry) {
+            const f = assemblySymmetry.getFeatures(assemblyId)
+            if (f._rowCount) {
+                symmetryFeatureId = f.id.value(0)
+            } else {
+                symmetryFeatureId = -1
+            }
+        } else {
+            symmetryFeatureId = -1
+        }
+        await createSymmetryRepr()
+    }
+
+    function getSymmetryFeatureIds() {
+        const symmetryFeatureIds: { id: number, label: string }[] = []
+        if (assemblySymmetry) {
+            const symmetryFeatures = assemblySymmetry.getFeatures(assemblyId)
+            for (let i = 0, il = symmetryFeatures._rowCount; i < il; ++i) {
+                const id = symmetryFeatures.id.value(i)
+                const symmetry = symmetryFeatures.symmetry_value.value(i)
+                const type = symmetryFeatures.type.value(i)
+                const stoichiometry = symmetryFeatures.stoichiometry_value.value(i)
+                const label = `${id}: ${symmetry} ${type} ${stoichiometry}`
+                symmetryFeatureIds.push({ id, label })
+            }
+        }
+        return symmetryFeatureIds
+    }
+
+    async function getStructure() {
+        if (model) structure = await getStructureFromModel(model, assemblyId)
+        if (model && structure) {
+            label = `${model.label} - Assembly ${assemblyId}`
+        } else {
+            label = ''
+        }
+        await createStructureRepr()
+    }
+
+    async function createStructureRepr() {
+        if (structure) {
+            await cartoon.create(structure, {
+                colorTheme: { name: 'chain-id' },
+                sizeTheme: { name: 'uniform', value: 0.2 },
+                useFog: false // TODO fog not working properly
+            }).run()
+
+            // await ballAndStick.create(structure, {
+            //     colorTheme: { name: 'element-symbol' },
+            //     sizeTheme: { name: 'uniform', value: 0.1 },
+            //     useFog: false // TODO fog not working properly
+            // }).run()
+
+            viewer.center(structure.boundary.sphere.center)
+        } else {
+            cartoon.destroy()
+            // ballAndStick.destroy()
+        }
+
+        viewer.add(cartoon)
+        // viewer.add(ballAndStick)
+    }
+
+    async function createSymmetryRepr() {
+        if (assemblySymmetry) {
+            const features = assemblySymmetry.getFeatures(assemblyId)
+            if (features._rowCount) {
+                const axesShape = getAxesShape(symmetryFeatureId, assemblySymmetry)
+                if (axesShape) {
+                    // getClusterColorTheme(symmetryFeatureId, assemblySymmetry)
+                    await axes.create(axesShape, {
+                        colorTheme: { name: 'shape-group' },
+                        // colorTheme: { name: 'uniform', value: Color(0xFFCC22) },
+                        useFog: false // TODO fog not working properly
+                    }).run()
+                } else {
+                    axes.destroy()
+                }
+            } else {
+                axes.destroy()
+            }
+        } else {
+            axes.destroy()
+        }
+        viewer.add(axes)
+        viewer.requestDraw()
+    }
+
+    await setModel(0, props.assembly, props.symmetryFeature)
 
     return {
-        model,
+        get label() { return label },
+        models,
         get structure() { return structure },
+        get assemblySymmetry() { return assemblySymmetry },
+
         cartoon,
-        ballAndStick,
+        // ballAndStick,
+        axes,
+
+        get modelId() { return modelId },
+        get assemblyId() { return assemblyId },
+        get symmetryFeatureId() { return symmetryFeatureId },
+
+        setAssembly,
+        getAssemblyIds,
+        setSymmetryFeature,
+        getSymmetryFeatureIds,
+
+        destroy: () => {
+            viewer.remove(cartoon)
+            // viewer.remove(ballAndStick)
+            viewer.remove(axes)
+            viewer.requestDraw()
 
-        setAssembly
+            cartoon.destroy()
+            // ballAndStick.destroy()
+            axes.destroy()
+        }
     }
 }